Là sao chép mã là một điều ác cần thiết trong C?


16

Tôi còn khá mới với C, và tôi tự hỏi liệu sao chép mã có phải là một điều xấu cần thiết khi viết các cấu trúc dữ liệu chung và C nói chung không?

Tôi có thể cố gắng viết một triển khai chung hash mapchẳng hạn, nhưng tôi luôn thấy kết quả cuối cùng là lộn xộn. Tôi cũng có thể viết một triển khai chuyên biệt chỉ cho trường hợp sử dụng cụ thể này, giữ cho mã rõ ràng và dễ đọc và gỡ lỗi. Điều thứ hai tất nhiên sẽ dẫn đến một số sao chép mã.

Là các triển khai chung là một định mức, hoặc bạn viết các triển khai khác nhau cho từng trường hợp sử dụng?


11
Tình huống khó khăn về đạo đức mà bạn đang mô tả không phải là duy nhất đối với C. Giai thoại, tôi thấy rằng thanh tạo ra một triển khai chung là khá cao, trừ khi bạn có khán giả cho nó. Lượng nỗ lực cần thiết để tạo ra một triển khai chung tốt vượt xa giải pháp điểm, IME.
Robert Harvey


1
@kevincline: Chà, Java không có khái quát. Cấp, đó là loại chung chung "không chú ý đến người đàn ông đằng sau bức màn".
Robert Harvey

1
Và trước khi tạo tướng, bạn có thể đã vượt qua trong Object và chuyển qua Float, Double, Integer và Long tương ứng.
Daniel Kaplan

3
Deja vu là một công cụ kiểm tra mùi mã và ưu tiên cho DRY là gốc rễ của tất cả các thực tiễn tốt nhất đáng chú ý đến IMO. Nhưng tôi đã tự bắn vào chân mình bằng các ngôn ngữ định hướng OOP cố gắng hết sức để tránh trùng lặp vì lợi ích của chính nó. Nếu bạn không ràng buộc các mối quan tâm không liên quan với nhau một cách không cần thiết hoặc phá hủy tính dễ đọc vì hai chức năng tương tự trở thành một, tôi sẽ đi theo bản năng trong hầu hết mọi ngôn ngữ.
Erik Reppen

Câu trả lời:


27

C gây khó khăn cho việc viết mã chung. Không giống như C ++, cung cấp cho bạn các mẫu và các hàm ảo, C chỉ có 3 cơ chế để viết mã chung:

  1. void* con trỏ
  2. Macro tiền xử lý
  3. Chức năng con trỏ

void* các con trỏ khác xa lý tưởng, vì bạn mất tất cả các loại an toàn do trình biên dịch cung cấp, điều này có thể dẫn đến hành vi khó xác định khó gỡ lỗi do các kiểu phôi không hợp lệ.

Các macro tiền xử lý có những nhược điểm đáng chú ý - mở rộng tiền xử lý về cơ bản chỉ là một cơ chế tìm / thay thế xảy ra trước giai đoạn biên dịch, một lần nữa có thể dẫn đến các lỗi khó gỡ lỗi. Ví dụ điển hình là một cái gì đó như : #define add(x) (x+x), nơi xcó thể được tăng gấp đôi nếu bạn gọi add(i++). Bạn có thể viết mã chung kiểu mẫu hoàn toàn bằng cách sử dụng macro C, nhưng kết quả thực sự gớm ghiếc và khó bảo trì .

Các con trỏ hàm cung cấp một cách tốt để viết mã chung, nhưng tiếc là chúng không cung cấp cho bạn tính tổng quát kiểu - chúng chỉ cung cấp khả năng đa hình thời gian chạy (ví dụ, tại sao, thư viện tiêu chuẩn qsortvẫn yêu cầu một hàm cần void*con trỏ.)

Bạn cũng có thể thực hiện phân cấp lớp trong C bằng cách sử dụng các cấu trúc, như được thực hiện trong thư viện GLib cung cấp một GObjectlớp cơ sở chung . Nhưng điều này gặp phải các vấn đề tương tự như sử dụng void*con trỏ, vì bạn vẫn cần phải dựa vào việc truyền thủ công có khả năng không an toàn để đúc lên và xuống.

Vì vậy, yeah, C làm cho nó khó viết mã vừa chung chung vừa an toàn / dễ bảo trì, điều không may có thể dẫn đến sao chép mã. Các dự án C lớn thường sử dụng các ngôn ngữ kịch bản để tạo mã lặp đi lặp lại trong quá trình xây dựng.


1
Người ta cũng có thể tạo mã C lặp đi lặp lại bằng cách sử dụng các ngôn ngữ / công cụ tạo khuôn mẫu bên ngoài hoặc tập lệnh shell hoặc sử dụng ant hoặc ...
Công việc

URL tiếng Nga khó hiểu này của Google mà bạn đã đặt trong câu trả lời của bạn là gì? Nhấp vào nó sẽ khiến lời nhắc "mở" xuất hiện trong trình duyệt của tôi. Cái này có an toàn không?
Robert Harvey

2
@RobertHarvey Quét virus và bảo mật của tôi nghĩ rằng nó ổn. Nó là một tệp Tiêu đề ngôn ngữ C, phần mở rộng .h. Nó xuất hiện hợp pháp.
maple_shaft

@maple_shaft: OK. Tôi đã xóa Googlization xung quanh liên kết.
Robert Harvey

Đây chỉ là một tệp .h (tiêu đề C) văn bản đơn giản
Charles Salvia

15

Tôi không thể nói cho người khác, nhưng theo kinh nghiệm cá nhân của tôi với C, sao chép mã không phải là vấn đề lớn. Cho dù điều này là do kích thước dự án, hoặc một bộ mẫu quyến rũ tôi không thể nói. Tuy nhiên, có ba quy tắc mà tôi tuân theo mà tôi nghĩ là có thể áp dụng. Không theo thứ tự cụ thể, họ ...

  1. Viết cho những gì cần thiết. Tính tổng quát có thể đến sau NẾU cần thiết.
  2. Nếu tính tổng quát là cần thiết, con trỏ void, con trỏ hàm và kích thước cấu trúc có thể là vô giá. Ví dụ, thường trình qsort () sử dụng cả ba.
  3. Cho trọng lượng để làm cho mã của bạn rõ ràng.

5

Điều đáng chú ý là bạn có thể không cần một giải pháp hoàn toàn chung chung để giảm trùng lặp mã. Đôi khi tái cấu trúc thông thường và một chút khái quát hóa phương pháp là đủ.

Khi viết một giải pháp tổng quát cho nhiều đối tượng, bạn phải xem xét:

  1. Những cách mới lạ nào người tiêu dùng có thể sử dụng mã của bạn và làm thế nào để bạn chứa chúng?
  2. Tôi cần bắt lỗi gì? Những lỗi tôi không nên bắt?
  3. API cần mạnh đến mức nào? Tôi cần cung cấp bao nhiêu quá tải?
  4. Tôi phải áp dụng các biện pháp bảo mật nào để mã không thể được sử dụng cho mục đích xấu?

Eric Lippert đã viết toàn bộ bài viết về một quyết định thiết kế duy nhất trong mã khung .NET. Nói chung, việc tái cấu trúc thường đơn giản hơn.


3

Tôi thường sử dụng một triển khai chung, như glib's , sau đó nếu quá trình phân phối kết thúc quá khó chịu, hãy tạo một trình bao bọc cụ thể cho từng loại nhỏ. Tuy nhiên, rất nhiều sự phân vai được mong đợi trong C, cũng như việc sử dụng void*một kiểu chung chung, vì vậy những gì được coi là "lộn xộn" trong ngôn ngữ khác chỉ là điển hình C. Đủ đáng sợ, nó sẽ trông tự nhiên hơn rất nhiều khi bạn có thêm kinh nghiệm trong ngôn ngữ


2
"Những gì sẽ được coi là 'lộn xộn' trong một ngôn ngữ khác chỉ là điển hình C. Đủ đáng sợ, nó sẽ trông tự nhiên hơn rất nhiều khi bạn có thêm kinh nghiệm về ngôn ngữ này." Ha, ha, ha! C luôn là một trong những ngôn ngữ yêu thích của tôi, nhưng điều đó vẫn khá buồn cười!
GlenPeterson

0

Vấn đề tái sử dụng của bạn không phải là duy nhất đối với C. Tuy nhiên, một số triển khai HashMap tuyệt vời và rất có thể tái sử dụng nằm ngoài đó bạn có thể dựa vào đó. Một trong hai hoặc hai thập kỷ đã có trong Java: http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

Tất cả các bộ sưu tập Java, bao gồm HashMap đang được viết lại cho Java 8 để loại bỏ ý tưởng về trình vòng lặp và thay vào đó yêu cầu bạn chuyển một hàm cho bộ sưu tập để nó có thể lặp lại bên trong. Đây là một chiến thắng lớn để viết mã đồng thời - bộ sưu tập có thể quản lý đồng thời trong nội bộ để khách hàng không phải làm vậy. Điều đó nói rằng, hầu hết các lập trình viên không được sử dụng để truyền con trỏ hàm. Điều này thực hiện tốt công việc giải thích nơi HashMap đang đứng đầu: http://cr.openjdk.java.net/~briangoetz/lambda/collections-overview.html

Cú pháp của Scala có thể khiến việc dịch một thứ như thế này sang C trở nên khó khăn, nhưng đây là một ví dụ về một bộ sưu tập đã hoạt động theo cách mà các bộ sưu tập Java 8 sẽ - không thay đổi theo mặc định, cung cấp các phương thức định dạng khác nhau, sử dụng các con trỏ hàm và khác những thứ hay ho như ứng dụng một phần: http://www.scala-lang.org/api/civerse/index.html#scala.collection.Map

Nếu bạn đang làm nhiều thứ với HashMaps và các cấu trúc dữ liệu nâng cao khác, bạn có thể muốn xem một ngôn ngữ hỗ trợ chúng.

Quay trở lại ý tưởng sao chép mã nói chung, bạn có thể viết mã rất nhanh bằng cách cắt và dán, nhưng nó tạo ra mã khủng. Tôi muốn nói rằng nó có thể chấp nhận được trong mã vứt đi như báo cáo một lần. Đôi khi cũng không sao khi không có dòng mã nào của nó, giống như mã UI chỉ xây dựng một màn hình mà không có gì khác phụ thuộc vào, nhưng thực sự đó là một khu vực rất xám.

Tôi không nghĩ rằng quá đơn giản hóa để nói rằng bất cứ khi nào bạn thấy mình sao chép mã, bạn nên tạo một hàm. Các lỗi mà bạn sửa trong một bản sao của mã và không phải là một lý do đủ để tạo ra một chức năng.


1
- "Tôi không nghĩ rằng quá đơn giản hóa để nói rằng bất cứ khi nào bạn thấy mình sao chép mã, bạn nên tạo một hàm" - Trong C đó là một sự đơn giản hóa quá mức. Hãy thử tạo một ngăn xếp trong C cho các số nguyên, nhân đôi và chuỗi mà không lặp lại các chức năng đẩy và bật cho từng loại dữ liệu.
mike30

@mike Không thể sử dụng con trỏ void?
sakisk

@mike - điểm tốt. Nhưng một điểm ngược lại là bạn có thể triển khai Stack cho từng loại dữ liệu theo cách mà bạn không cần phải thực hiện lại Stack. Vì vậy, mục tiêu không phải là numberOfImcellenceations = 1, mà giống như 5. Bạn có thể làm cho việc inttriển khai thực hiện cho char... Đã quá lâu kể từ khi tôi sử dụng C mà tôi không thể nhớ.
GlenPeterson

1
@faif. Có, nhưng nhu cầu sử dụng con trỏ void làm cho điểm của C. Con trỏ đúc chậm hơn và kém thanh lịch hơn so với các lựa chọn thay thế được tìm thấy trong C ++. Sao chép mã xếp hạng cao hơn một chút trong danh sách các tùy chọn có thể có khi mã hóa trong C.
mike30

@ mike30 Có bất kỳ bằng chứng / tài liệu tham khảo nào về: "Đúc con trỏ trong C chậm hơn các lựa chọn thay thế được tìm thấy trong C ++ không?"
sakisk

0

Tôi đang tự hỏi liệu sao chép mã có phải là một điều ác cần thiết khi viết các cấu trúc dữ liệu chung và C nói chung không?

Trong C, hoàn toàn đối với tôi, như một người nảy giữa C và C ++. Tôi chắc chắn sao chép nhiều thứ tầm thường hơn trên cơ sở hàng ngày trong C so với C ++, nhưng cố tình và tôi không nhất thiết coi đó là "xấu xa" bởi vì có ít nhất một số lợi ích thiết thực - tôi nghĩ đó là một sai lầm khi xem xét tất cả mọi thứ như "tốt" hay "xấu" - tất cả mọi thứ là vấn đề đánh đổi. Hiểu rõ sự đánh đổi đó là chìa khóa để không tránh những quyết định đáng tiếc trong nhận thức muộn màng và chỉ dán nhãn những thứ là "tốt" hay "xấu" thường bỏ qua tất cả những điều tinh tế như vậy.

Mặc dù vấn đề không phải là duy nhất đối với C như những người khác đã chỉ ra, nhưng nó có thể trở nên trầm trọng hơn đáng kể ở C do thiếu bất cứ thứ gì tao nhã hơn macro hoặc con trỏ trống đối với các khái quát, sự lúng túng của OOP không tầm thường, và thực tế là Thư viện chuẩn C không đi kèm với bất kỳ container nào. Trong C ++, một người thực hiện danh sách liên kết của riêng họ có thể khiến một đám đông giận dữ yêu cầu tại sao họ không sử dụng thư viện chuẩn, trừ khi họ là sinh viên. Trong C, bạn sẽ mời một đám đông giận dữ nếu bạn không thể tự tin triển khai một triển khai danh sách liên kết thanh lịch trong giấc ngủ của mình vì một lập trình viên C thường được mong đợi ít nhất có thể làm những việc đó hàng ngày. Nó ' Không phải do một số ám ảnh kỳ lạ trong các danh sách được liên kết mà Linus Torvalds đã sử dụng việc thực hiện tìm kiếm và loại bỏ SLL bằng cách sử dụng hai lần như một tiêu chí để đánh giá một lập trình viên hiểu ngôn ngữ và có "khẩu vị tốt". Đó là bởi vì các lập trình viên C có thể được yêu cầu thực hiện logic như vậy hàng ngàn lần trong sự nghiệp của họ. Trong trường hợp này đối với C, nó giống như một đầu bếp đánh giá các kỹ năng của một đầu bếp mới bằng cách khiến họ chỉ cần chuẩn bị một số trứng để xem liệu ít nhất họ có nắm vững những điều cơ bản mà họ sẽ phải làm mọi lúc không.

Ví dụ: có lẽ tôi đã triển khai cấu trúc dữ liệu "danh sách tự do được lập chỉ mục" cơ bản này hàng chục lần trong C cục bộ cho mỗi trang web sử dụng chiến lược phân bổ này (gần như tất cả các cấu trúc được liên kết của tôi để tránh phân bổ một nút tại một thời điểm và giảm một nửa bộ nhớ chi phí của các liên kết trên 64-bit):

nhập mô tả hình ảnh ở đây

Nhưng trong C, nó chỉ cần một lượng rất nhỏ mã cho reallocmột mảng có thể phát triển và gộp một số bộ nhớ từ nó bằng cách sử dụng một cách tiếp cận được lập chỉ mục vào một danh sách miễn phí khi thực hiện cấu trúc dữ liệu mới sử dụng cấu trúc dữ liệu mới này.

Bây giờ tôi có điều tương tự được triển khai trong C ++ và ở đó tôi chỉ thực hiện nó một lần dưới dạng mẫu lớp. Nhưng đó là một triển khai phức tạp hơn nhiều về phía C ++ với hàng trăm dòng mã và một số phụ thuộc bên ngoài cũng trải dài hàng trăm dòng mã. Và lý do chính khiến nó phức tạp hơn nhiều là vì tôi phải viết mã theo ý tưởng Tcó thể là bất kỳ loại dữ liệu nào có thể. Nó có thể ném bất cứ lúc nào (trừ khi phá hủy nó, điều mà tôi phải làm rõ ràng như với các thùng chứa thư viện tiêu chuẩn), tôi đã phải suy nghĩ về việc căn chỉnh phù hợp để phân bổ bộ nhớ choT . và tôi phải thêm các trình vòng lặp, cả các trình vòng lặp có thể thay đổi và chỉ đọc (const), v.v.

Mảng có thể phát triển Không phải là Khoa học tên lửa

Trong C ++, mọi người nghe có vẻ như std::vectorlà công việc của một nhà khoa học tên lửa, được tối ưu hóa cho đến chết, nhưng nó không hoạt động tốt hơn một mảng C động được mã hóa theo một loại dữ liệu cụ thể chỉ sử dụng reallocđể tăng dung lượng mảng khi đẩy lùi bằng một hàng chục dòng mã. Sự khác biệt là việc triển khai rất phức tạp để tạo ra một chuỗi truy cập ngẫu nhiên có thể phát triển hoàn toàn tuân thủ tiêu chuẩn, tránh gọi các công cụ trên các phần tử không được xác nhận, an toàn ngoại lệ, cung cấp cả các trình lặp truy cập ngẫu nhiên và không truy cập ngẫu nhiên, loại sử dụng những đặc điểm để phân tán các phần tử điền từ các phần tử phạm vi cho các loại tích phân nhất định củaT, có khả năng xử lý các POD khác nhau bằng cách sử dụng các đặc điểm loại, v.v ... v.v. Tại thời điểm đó, bạn thực sự cần một triển khai rất phức tạp chỉ để tạo ra một mảng động có thể phát triển, nhưng chỉ vì nó cố gắng xử lý mọi trường hợp sử dụng có thể tưởng tượng được. Về mặt tích cực, bạn có thể nhận được rất nhiều dặm từ tất cả những nỗ lực đó nếu bạn thực sự cần lưu trữ cả POD và UDT không tầm thường, đã sử dụng cho các thuật toán dựa trên trình lặp chung hoạt động trên mọi cấu trúc dữ liệu tuân thủ, được hưởng lợi từ việc xử lý ngoại lệ và RAII, ít nhất là đôi khi ghi đè std::allocatorbằng bộ cấp phát tùy chỉnh của riêng bạn, v.v ... Nó chắc chắn sẽ được đền đáp trong thư viện tiêu chuẩn khi bạn xem xét có bao nhiêu lợi íchstd::vector đã có trên toàn bộ thế giới của những người đã sử dụng nó, nhưng đó là thứ được thực hiện trong thư viện tiêu chuẩn được thiết kế để nhắm vào toàn bộ nhu cầu của thế giới.

Xử lý đơn giản hơn Các trường hợp sử dụng rất cụ thể

Kết quả là chỉ xử lý các trường hợp sử dụng rất cụ thể với "danh sách miễn phí được lập chỉ mục" của tôi, mặc dù đã triển khai danh sách miễn phí này hàng chục lần ở phía C và kết quả là có một số mã tầm thường được sao chép, tôi có thể đã viết ít mã hơn tổng cộng trong C để thực hiện rằng hàng chục lần tôi phải thực hiện nó chỉ một lần trong C ++ và tôi đã phải dành ít thời gian hơn để duy trì hàng tá triển khai C đó hơn là tôi phải duy trì việc thực hiện C ++ đó. Một trong những lý do chính khiến phía C đơn giản là vì tôi thường làm việc với POD trong C bất cứ khi nào tôi sử dụng kỹ thuật này và tôi thường không cần nhiều chức năng hơn inserterasetại các trang web cụ thể mà tôi thực hiện điều này tại địa phương. Về cơ bản tôi chỉ có thể triển khai tập hợp con nhỏ nhất của chức năng mà phiên bản C ++ cung cấp, vì tôi có thể tự do đưa ra rất nhiều giả định về những gì tôi làm và không cần thiết kế khi tôi triển khai nó cho một mục đích sử dụng rất cụ thể trường hợp

Giờ đây, phiên bản C ++ đẹp hơn và an toàn hơn rất nhiều để sử dụng, nhưng nó vẫn là một PITA chính để thực hiện và thực hiện tuân thủ vòng lặp ngoại lệ và an toàn hai chiều, ví dụ, theo cách mà việc đưa ra một triển khai chung, có thể tái sử dụng nhiều thời gian hơn nó thực sự tiết kiệm trong trường hợp này. Và rất nhiều chi phí để thực hiện nó một cách khái quát bị lãng phí không chỉ trả trước mà còn lặp đi lặp lại dưới dạng những thứ như thời gian xây dựng leo thang được trả nhiều lần mỗi ngày.

Không phải là một cuộc tấn công vào C ++!

Nhưng đây không phải là một cuộc tấn công vào C ++ vì tôi yêu C ++, nhưng khi nói đến cấu trúc dữ liệu, tôi đã ưu tiên C ++ cho các cấu trúc dữ liệu thực sự không tầm thường mà tôi muốn dành nhiều thời gian trả trước để thực hiện một cách rất khái quát, làm cho ngoại lệ an toàn đối với tất cả các loại có thể T, làm cho tuân thủ tiêu chuẩn và lặp lại, v.v., trong đó loại chi phí trả trước đó thực sự được đền đáp dưới dạng một tấn dặm.

Tuy nhiên, điều đó cũng thúc đẩy một tư duy thiết kế rất khác nhau. Trong C ++ nếu tôi muốn tạo Octree để phát hiện va chạm, tôi có xu hướng muốn khái quát hóa nó đến mức thứ n. Tôi không chỉ muốn làm cho nó lưu trữ các lưới tam giác được lập chỉ mục. Tại sao tôi nên giới hạn nó chỉ ở một loại dữ liệu mà nó có thể hoạt động khi tôi có một cơ chế tạo mã siêu mạnh trong tầm tay để loại bỏ tất cả các hình phạt trừu tượng khi chạy? Tôi muốn nó lưu trữ các hình cầu thủ tục, hình khối, voxels, bề mặt NURB, đám mây điểm, v.v. và cố gắng làm cho nó tốt cho mọi thứ, bởi vì thật hấp dẫn khi muốn thiết kế nó theo cách đó khi bạn có các mẫu trong tầm tay. Tôi thậm chí có thể không muốn giới hạn nó để phát hiện va chạm - làm thế nào về raytracing, chọn, vv? C ++ làm cho nó ban đầu trông "dễ dàng" để khái quát cấu trúc dữ liệu đến mức thứ n. Và đó là cách tôi sử dụng để thiết kế các chỉ mục không gian như vậy trong C ++. Tôi đã cố gắng thiết kế chúng để xử lý toàn bộ nhu cầu đói của thế giới và những gì tôi nhận được thường là "jack của tất cả các giao dịch" với mã cực kỳ phức tạp để cân bằng với mọi trường hợp sử dụng có thể tưởng tượng được.

Mặc dù vậy, thật thú vị, tôi đã sử dụng lại nhiều hơn các chỉ số không gian mà tôi đã thực hiện trong C trong những năm qua, và không có lỗi của C ++, nhưng chỉ khai thác những gì mà ngôn ngữ cám dỗ tôi làm. Khi tôi mã hóa một cái gì đó như một octree trong C, tôi có xu hướng chỉ làm cho nó hoạt động với các điểm và hài lòng với điều đó, bởi vì ngôn ngữ làm cho nó quá khó để thậm chí cố gắng khái quát nó đến mức thứ n. Nhưng do những xu hướng đó, tôi đã có xu hướng thiết kế mọi thứ trong nhiều năm thực sự hiệu quả và đáng tin cậy hơn và thực sự phù hợp với một số nhiệm vụ nhất định, vì chúng không bận tâm đến việc nói chung ở cấp độ thứ n. Họ trở thành con át chủ bài trong một danh mục chuyên ngành thay vì người tham gia tất cả các ngành nghề. Một lần nữa, điều đó không có lỗi của C ++ mà chỉ đơn giản là xu hướng con người tôi có khi tôi sử dụng nó trái ngược với C.

Nhưng dù sao, tôi yêu cả hai ngôn ngữ nhưng có những xu hướng khác nhau. Trong CI có xu hướng không khái quát đủ. Trong C ++ tôi có xu hướng khái quát hóa quá nhiều. Sử dụng cả hai loại đã giúp tôi cân bằng bản thân.

Là các triển khai chung là một định mức, hoặc bạn viết các triển khai khác nhau cho từng trường hợp sử dụng?

Đối với những thứ tầm thường như các danh sách được lập chỉ mục 32 bit được liên kết đơn bằng cách sử dụng các nút từ một mảng hoặc một mảng tự phân bổ lại (tương tự như std::vectortrong C ++) hoặc, nói, một octree chỉ lưu trữ các điểm và nhằm mục đích không làm gì thêm, tôi không ' t bận tâm để khái quát đến điểm lưu trữ bất kỳ loại dữ liệu. Tôi thực hiện những điều này để lưu trữ một kiểu dữ liệu cụ thể (mặc dù nó có thể trừu tượng và sử dụng các con trỏ hàm trong một số trường hợp, nhưng ít nhất là cụ thể hơn so với gõ vịt với đa hình tĩnh).

Và tôi hoàn toàn hài lòng với một chút dư thừa trong những trường hợp đó với điều kiện tôi phải kiểm tra kỹ lưỡng. Nếu tôi không kiểm tra đơn vị, thì sự dư thừa bắt đầu cảm thấy khó chịu hơn nhiều, bởi vì bạn có thể có mã dự phòng có thể trùng lặp lỗi, ví dụ: Ngay cả khi loại mã bạn viết không có khả năng cần thay đổi thiết kế, nó vẫn có thể cần thay đổi vì nó bị hỏng. Tôi có xu hướng viết các bài kiểm tra đơn vị kỹ lưỡng hơn cho mã C tôi viết như một lý do.

Đối với những thứ không cần thiết, đó thường là khi tôi tiếp cận với C ++, nhưng nếu tôi triển khai nó trong C, tôi sẽ cân nhắc sử dụng chỉ void*con trỏ, có thể chấp nhận kích thước loại để biết phân bổ bộ nhớ cho mỗi phần tử và có thể là copy/destroycon trỏ chức năng để sao chép sâu và hủy dữ liệu nếu nó không có khả năng xây dựng / phá hủy tầm thường. Hầu hết thời gian tôi không bận tâm và không sử dụng quá nhiều C để tạo ra các cấu trúc dữ liệu và thuật toán phức tạp nhất.

Nếu bạn sử dụng một cấu trúc dữ liệu đủ thường xuyên với một loại dữ liệu cụ thể, bạn cũng có thể bọc một phiên bản an toàn kiểu trên một phiên bản chỉ hoạt động với bit và byte và con trỏ hàm và void*, ví dụ, để xác định lại mức độ an toàn của kiểu thông qua trình bao bọc C.

Tôi có thể cố gắng viết một triển khai chung cho bản đồ băm chẳng hạn, nhưng tôi luôn thấy kết quả cuối cùng là lộn xộn. Tôi cũng có thể viết một triển khai chuyên biệt chỉ cho trường hợp sử dụng cụ thể này, giữ cho mã rõ ràng và dễ đọc và gỡ lỗi. Điều thứ hai tất nhiên sẽ dẫn đến một số sao chép mã.

Các bảng băm là loại iffy vì nó có thể không quan trọng để thực hiện hoặc thực sự phức tạp tùy thuộc vào mức độ phức tạp của nhu cầu của bạn đối với băm, thử lại, nếu bạn cần tự động phát triển bảng một cách ngầm định hoặc có thể dự đoán kích thước bảng trong nâng cao, cho dù bạn sử dụng địa chỉ mở hay chuỗi riêng biệt, v.v. Nhưng một điều cần lưu ý là nếu bạn điều chỉnh một bảng băm hoàn hảo theo nhu cầu của một trang web cụ thể, thì việc triển khai thường sẽ rất phức tạp và thường thắng Sẽ không quá dư thừa khi nó được điều chỉnh chính xác cho những nhu cầu đó. Ít nhất đó là cái cớ tôi tự đưa ra nếu tôi thực hiện một cái gì đó tại địa phương. Nếu không, bạn có thể chỉ sử dụng phương thức được mô tả ở trên void*và các con trỏ hàm để sao chép / hủy bỏ mọi thứ và khái quát hóa nó.

Thông thường, sẽ không mất nhiều nỗ lực hoặc nhiều mã để đánh bại cấu trúc dữ liệu rất khái quát nếu phương án thay thế của bạn cực kỳ áp dụng cho trường hợp sử dụng chính xác của bạn. Ví dụ, việc đánh bại hiệu năng sử dụng malloccho từng nút ( hoàn toàn không đáng kể ), một lần và đối với tất cả các mã bộ nhớ cho nhiều nút) một lần và đối với mã, bạn không bao giờ phải xem lại trường hợp sử dụng rất chính xác ngay cả khi thực hiện mới hơn mallocđi ra. Có thể mất cả đời để đánh bại nó và mã hóa không kém phần phức tạp mà bạn phải dành một phần lớn cuộc sống của mình để duy trì và cập nhật nếu bạn muốn phù hợp với tính tổng quát của nó.

Một ví dụ khác, tôi thường thấy việc thực hiện các giải pháp nhanh hơn hoặc gấp 10 lần so với các giải pháp VFX do Pixar hoặc Dreamworks cung cấp là cực kỳ dễ dàng. Tôi có thể làm điều đó trong giấc ngủ của tôi. Nhưng đó không phải là vì triển khai của tôi là vượt trội - xa, xa nó. Họ hoàn toàn thấp kém đối với hầu hết mọi người. Chúng chỉ vượt trội đối với các trường hợp sử dụng rất, rất cụ thể của tôi. Các phiên bản của tôi rất xa, thường ít áp dụng hơn so với Pixar's hoặc Dreamwork. Đó là một so sánh vô lý không công bằng vì các giải pháp của họ hoàn toàn xuất sắc so với các giải pháp đơn giản ngu ngốc của tôi, nhưng đó là loại quan điểm. Việc so sánh không cần phải công bằng. Nếu tất cả những gì bạn cần là một vài thứ rất cụ thể, bạn không cần phải tạo cấu trúc dữ liệu xử lý một danh sách vô tận những thứ bạn không cần.

Đồng nhất bit và byte

Một điều cần khai thác trong C vì nó thiếu sự an toàn kiểu vốn có là ý tưởng lưu trữ mọi thứ một cách đồng nhất dựa trên các đặc tính của bit và byte. Có nhiều sự mờ ảo hơn do kết quả giữa cấp phát bộ nhớ và cấu trúc dữ liệu.

Nhưng lưu trữ một loạt các thứ có kích thước thay đổi, hoặc thậm chí những thứ chỉ có thể có kích thước thay đổi, như đa hình DogCat, rất khó để thực hiện một cách hiệu quả. Bạn không thể đi theo giả định rằng chúng có thể có kích thước thay đổi và lưu trữ chúng liên tục trong một thùng chứa truy cập ngẫu nhiên đơn giản vì sải chân để đi từ yếu tố này sang yếu tố tiếp theo có thể khác nhau. Do đó, để lưu trữ một danh sách chứa cả chó và mèo, bạn có thể phải sử dụng 3 trường hợp phân bổ / cấu trúc dữ liệu riêng biệt (một cho chó, một cho mèo và một cho danh sách đa hình về con trỏ cơ sở hoặc con trỏ thông minh hoặc tệ hơn , phân bổ mỗi con chó và con mèo theo một công cụ phân bổ mục đích chung và phân tán chúng khắp bộ nhớ), điều này sẽ gây tốn kém và phải chịu một phần lỗi nhớ cache nhân lên.

Vì vậy, một chiến lược để sử dụng trong C, mặc dù có mức độ phong phú và an toàn giảm, là khái quát hóa ở mức độ bit và byte. Bạn có thể giả sử rằng DogsCatsyêu cầu cùng số bit và byte, có cùng các trường, cùng một con trỏ tới một bảng con trỏ hàm. Nhưng đổi lại, sau đó bạn có thể mã hóa các cấu trúc dữ liệu ít hơn, nhưng cũng quan trọng, lưu trữ tất cả những thứ này một cách hiệu quả và liên tục. Bạn đang đối xử với chó và mèo như các hiệp hội tương tự trong trường hợp đó (hoặc bạn thực sự có thể sử dụng một liên minh).

Và điều đó đi kèm với một chi phí rất lớn để gõ an toàn. Nếu có một điều tôi nhớ nhiều hơn bất cứ điều gì khác trong C, thì đó là sự an toàn. Nó tiến gần hơn đến mức lắp ráp trong đó các cấu trúc chỉ cho biết mức độ phân bổ bộ nhớ và cách mỗi trường dữ liệu được căn chỉnh. Nhưng đó thực sự là lý do số một của tôi để sử dụng C. Nếu bạn thực sự cố gắng kiểm soát bố cục bộ nhớ và nơi mọi thứ được phân bổ và nơi mọi thứ được lưu trữ tương đối với nhau, thường sẽ giúp bạn nghĩ về mọi thứ ở cấp độ bit và byte và bao nhiêu bit và byte bạn cần để giải quyết một vấn đề cụ thể. Ở đó, sự ngu ngốc của hệ thống loại C thực sự có thể trở nên có lợi hơn là một sự bất lợi. Thông thường, điều đó sẽ dẫn đến việc xử lý ít loại dữ liệu hơn,

Ảo tưởng / trùng lặp rõ ràng

Bây giờ tôi đã sử dụng "sao chép" theo nghĩa lỏng lẻo cho những thứ thậm chí không dư thừa. Tôi đã thấy mọi người phân biệt các thuật ngữ như trùng lặp "ngẫu nhiên / rõ ràng" với "trùng lặp thực tế". Theo cách tôi thấy thì không có sự phân biệt rõ ràng như vậy trong nhiều trường hợp. Tôi thấy sự khác biệt giống như "tính duy nhất tiềm năng" so với "sự trùng lặp tiềm năng" và nó có thể đi theo bất kỳ cách nào. Nó thường phụ thuộc vào cách bạn muốn thiết kế và triển khai của mình phát triển và cách chúng được thiết kế hoàn hảo cho một trường hợp sử dụng cụ thể. Nhưng tôi thường thấy rằng những gì có vẻ là sao chép mã sau đó hóa ra không còn dư thừa sau vài lần cải tiến.

Hãy thực hiện một mảng có thể phát triển đơn giản bằng cách sử dụng realloc, tương đương với std::vector<int>. Ban đầu, nó có thể là dư thừa với, sử dụng std::vector<int>trong C ++. Nhưng bạn có thể thấy, thông qua việc đo lường, có thể có ích khi phân bổ trước 64 byte để cho phép mười sáu số nguyên 32 bit được chèn mà không yêu cầu phân bổ heap. Bây giờ nó không còn dư thừa, ít nhất là không std::vector<int>. Và sau đó bạn có thể nói, "Nhưng tôi chỉ có thể khái quát hóa điều này thành một điều mới SmallVector<int, 16>, và bạn có thể. Nhưng sau đó, hãy nói rằng bạn thấy nó hữu ích bởi vì những mảng này rất nhỏ để tăng gấp bốn lần dung lượng mảng trên phân bổ heap thay vì tăng 1,5 (gần bằng số tiền mà nhiềuvectorviệc triển khai sử dụng) trong khi thực hiện giả định rằng dung lượng mảng luôn là sức mạnh của hai. Bây giờ container của bạn thực sự khác biệt, và có lẽ không có container nào giống như vậy. Và có lẽ bạn có thể cố gắng khái quát hóa các hành vi như vậy bằng cách thêm ngày càng nhiều tham số mẫu để tùy chỉnh độ nặng của preallocation, tùy chỉnh hành vi phân bổ lại, v.v., nhưng tại thời điểm đó, bạn có thể tìm thấy thứ gì đó thực sự khó sử dụng so với hàng tá dòng C đơn giản mã.

Và bạn thậm chí có thể đạt đến điểm cần cấu trúc dữ liệu phân bổ bộ nhớ đệm và đệm 256-bit, lưu trữ PODs riêng cho hướng dẫn AVX 256, phân bổ 128 byte để tránh phân bổ heap cho kích thước đầu vào nhỏ trong trường hợp phổ biến, tăng gấp đôi dung lượng khi đầy đủ và cho phép ghi đè an toàn các phần tử theo dõi vượt quá kích thước mảng nhưng không vượt quá dung lượng mảng. Vào thời điểm đó, nếu bạn vẫn đang cố gắng khái quát hóa một giải pháp để tránh sao chép một lượng nhỏ mã C, các vị thần lập trình có thể thương xót tâm hồn bạn.

Vì vậy, cũng có những lúc như thế này khi mà những thứ ban đầu bắt đầu trông có vẻ bắt đầu phát triển, khi bạn điều chỉnh một giải pháp để tốt hơn và tốt hơn và phù hợp hơn với một trường hợp sử dụng nhất định, thành một thứ hoàn toàn độc đáo và không dư thừa. Nhưng điều đó chỉ dành cho những thứ mà bạn có thể đủ khả năng để điều chỉnh chúng một cách hoàn hảo cho một trường hợp sử dụng cụ thể. Đôi khi chúng ta chỉ cần một thứ "đàng hoàng" được khái quát hóa cho mục đích của chúng ta và ở đó tôi được hưởng lợi nhiều nhất từ ​​các cấu trúc dữ liệu rất khái quát. Nhưng đối với những điều đặc biệt được thực hiện hoàn hảo cho một trường hợp sử dụng cụ thể, ý tưởng về "mục đích chung" và "được thực hiện hoàn hảo cho mục đích của tôi" bắt đầu trở nên quá không tương thích.

POD và nguyên thủy

Bây giờ trong C, tôi thường tìm thấy lý do để lưu trữ POD và đặc biệt là nguyên thủy vào cấu trúc dữ liệu bất cứ khi nào có thể. Điều đó có vẻ giống như một mô hình chống nhưng tôi thực sự thấy nó vô tình hữu ích trong việc cải thiện khả năng duy trì của mã so với các loại điều tôi thường làm trong C ++.

Một ví dụ đơn giản là thực hiện các chuỗi ngắn (như trường hợp thường có các chuỗi được sử dụng cho các khóa tìm kiếm - chúng có xu hướng rất ngắn). Tại sao phải xử lý tất cả các chuỗi có độ dài thay đổi có kích thước khác nhau trong thời gian chạy, ngụ ý xây dựng và phá hủy không tầm thường (vì chúng ta có thể cần phải phân bổ đống và miễn phí)? Làm thế nào về việc chỉ lưu trữ những thứ này trong một cấu trúc dữ liệu trung tâm, như bảng băm hoặc bảng băm an toàn luồng được thiết kế chỉ để thực hiện chuỗi, và sau đó tham khảo các chuỗi đó với một chuỗi cũ int32_thoặc:

struct IternedString 
{
    int32_t index;
};

... trong các bảng băm, cây đỏ đen, danh sách bỏ qua, v.v., nếu chúng ta không cần phân loại từ vựng? Bây giờ tất cả các cấu trúc dữ liệu khác mà chúng tôi đã mã hóa để hoạt động với các số nguyên 32 bit giờ đây có thể lưu trữ các khóa chuỗi được gắn vào này, thực tế chỉ là 32 bit ints. Và tôi đã tìm thấy trong các trường hợp sử dụng của mình ít nhất (có thể chỉ là miền của tôi vì tôi làm việc trong các lĩnh vực như raytracing, xử lý lưới, xử lý hình ảnh, hệ thống hạt, liên kết với ngôn ngữ kịch bản, triển khai bộ công cụ GUI đa luồng cấp thấp, v.v. - Những thứ cấp thấp nhưng không phải là cấp thấp như HĐH), rằng mã tình cờ xảy ra để trở nên hiệu quả hơn và đơn giản hơn chỉ là lưu trữ các chỉ số cho những thứ như thế này. Điều đó khiến tôi rất thường xuyên làm việc, nói 75% thời gian, chỉ int32_tfloat32 trong các cấu trúc dữ liệu không tầm thường của tôi hoặc chỉ lưu trữ những thứ có cùng kích thước (hầu như luôn luôn là 32 bit).

Và một cách tự nhiên nếu điều đó áp dụng cho trường hợp của bạn, bạn có thể tránh có một số triển khai cấu trúc dữ liệu cho các loại dữ liệu khác nhau, vì bạn sẽ làm việc với rất ít ở nơi đầu tiên.

Kiểm tra và độ tin cậy

Một điều cuối cùng tôi cung cấp, và nó có thể không dành cho tất cả mọi người, là ưu tiên kiểm tra viết cho các cấu trúc dữ liệu đó. Làm cho họ thực sự tốt ở một cái gì đó. Hãy chắc chắn rằng chúng cực kỳ đáng tin cậy.

Một số sao chép mã nhỏ trở nên dễ tha thứ hơn trong các trường hợp đó, vì sao chép mã chỉ là gánh nặng bảo trì nếu bạn phải thực hiện thay đổi xếp tầng cho mã trùng lặp. Bạn loại bỏ một trong những lý do chính để mã dự phòng như vậy thay đổi bằng cách đảm bảo rằng nó cực kỳ đáng tin cậy và thực sự phù hợp với những gì nó không làm.

Cảm giác thẩm mỹ của tôi đã thay đổi qua nhiều năm. Tôi không còn cảm thấy khó chịu vì tôi thấy một thư viện triển khai sản phẩm chấm hoặc một số logic SLL tầm thường đã được triển khai trong một thư viện khác. Tôi chỉ bị kích thích khi mọi thứ được kiểm tra kém và không đáng tin cậy, và tôi đã thấy rằng một suy nghĩ hiệu quả hơn nhiều. Tôi đã thực sự xử lý các cơ sở mã đã sao chép các lỗi thông qua mã trùng lặp và đã thấy các trường hợp tồi tệ nhất của mã hóa sao chép và dán làm cho một sự thay đổi tầm thường thành một vị trí trung tâm biến thành một thay đổi dễ bị lỗi đối với nhiều người. Tuy nhiên, nhiều lần trong số đó, đó là kết quả của việc kiểm tra kém, mã không trở nên đáng tin cậy và tốt ở những gì nó đang làm ở nơi đầu tiên. Trước đây khi tôi đang làm việc trong các cơ sở mã lỗi, tâm trí của tôi liên kết tất cả các hình thức sao chép mã là có xác suất trùng lặp lỗi rất cao và yêu cầu thay đổi xếp tầng. Tuy nhiên, một thư viện thu nhỏ thực hiện một điều cực kỳ tốt và đáng tin cậy sẽ tìm thấy rất ít lý do để thay đổi trong tương lai, ngay cả khi nó có một số mã trông có vẻ dư thừa ở đây và đó. Các ưu tiên của tôi đã bị tắt khi đó sự trùng lặp làm tôi khó chịu hơn là chất lượng kém và thiếu kiểm tra. Những điều sau nên được ưu tiên hàng đầu.

Mã sao chép cho chủ nghĩa tối giản?

Đây là một ý nghĩ hài hước xuất hiện trong đầu tôi, nhưng hãy xem xét trường hợp chúng ta có thể gặp thư viện C và C ++, điều này gần giống nhau: cả hai đều có cùng chức năng, cùng một lượng xử lý lỗi, một lỗi không đáng kể hiệu quả hơn các loại khác, v.v. Và quan trọng nhất là cả hai đều được triển khai thành công, được thử nghiệm tốt và đáng tin cậy. Thật không may, tôi phải nói một cách giả thuyết ở đây vì tôi chưa bao giờ tìm thấy bất cứ điều gì gần với một so sánh song song hoàn hảo. Nhưng những điều gần gũi nhất mà tôi từng tìm thấy trong sự so sánh song song này thường có thư viện C nhỏ hơn nhiều, nhỏ hơn nhiều so với C ++ (đôi khi bằng 1/10 kích thước mã của nó).

Và tôi tin rằng lý do cho điều đó là bởi vì, một lần nữa, để giải quyết vấn đề theo cách chung xử lý phạm vi sử dụng rộng nhất thay vì một trường hợp sử dụng chính xác có thể cần hàng trăm đến hàng nghìn dòng mã, trong khi trường hợp sau chỉ có thể yêu cầu một tá. Mặc dù có sự dư thừa và mặc dù thực tế là thư viện chuẩn C rất tệ khi cung cấp các cấu trúc dữ liệu tiêu chuẩn, nhưng cuối cùng thường tạo ra ít mã hơn trong tay con người để giải quyết các vấn đề tương tự, và tôi nghĩ rằng điều đó chủ yếu là do đến sự khác biệt về xu hướng của con người giữa hai ngôn ngữ này. Một người thúc đẩy giải quyết mọi thứ chống lại trường hợp sử dụng rất cụ thể, người kia có xu hướng thúc đẩy các giải pháp trừu tượng và chung chung hơn đối với phạm vi sử dụng rộng nhất, nhưng kết quả cuối cùng của những trường hợp này '

Tôi đã xem xét raytracer của ai đó trên github vào một ngày khác và nó đã được triển khai trong C ++ và yêu cầu rất nhiều, vì vậy rất nhiều mã cho một raytracer đồ chơi. Và tôi đã không dành nhiều thời gian để xem mã nhưng có một lượng lớn các cấu trúc có mục đích chung trong đó có cách xử lý, nhiều hơn những gì một raytracer sẽ cần. Và tôi nhận ra phong cách mã hóa đó bởi vì tôi đã từng sử dụng C ++ theo cách tương tự theo kiểu siêu từ dưới lên, tập trung vào việc tạo ra một thư viện đầy đủ các cấu trúc dữ liệu có mục đích rất chung trước tiên vượt lên trên và vượt ra ngoài vấn đề trong tầm tay và sau đó giải quyết vấn đề thực tế thứ hai. Nhưng trong khi các cấu trúc chung đó có thể loại bỏ một số dư thừa ở đây và ở đó và tận hưởng rất nhiều tái sử dụng trong bối cảnh mới, đổi lại, họ thổi phồng dự án rất lớn bằng cách trao đổi một chút dư thừa với một khối lượng mã / chức năng không cần thiết, và cái sau không nhất thiết phải dễ bảo trì hơn cái trước. Ngược lại, tôi thường thấy khó bảo trì hơn, vì thật khó để duy trì một thiết kế của một cái gì đó chung chung để thắt chặt các quyết định thiết kế cân bằng đối với các nhu cầu rộng nhất có thể.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.