Tại sao các cấu trúc và các lớp tách biệt các khái niệm trong C #?


44

Trong khi lập trình bằng C #, tôi tình cờ thấy một quyết định thiết kế ngôn ngữ lạ mà tôi không thể hiểu được.

Vì vậy, C # (và CLR) có hai loại dữ liệu tổng hợp: struct(loại giá trị, được lưu trữ trên ngăn xếp, không có kế thừa) và class(loại tham chiếu, được lưu trữ trên heap, có tính kế thừa).

Thiết lập này ban đầu nghe có vẻ hay, nhưng sau đó bạn tình cờ phát hiện ra một phương thức lấy tham số của loại tổng hợp và để biết liệu nó thực sự thuộc loại giá trị hay loại tham chiếu, bạn phải tìm khai báo loại của nó. Nó có thể nhận được thực sự khó hiểu đôi khi.

Giải pháp được chấp nhận chung cho vấn đề này dường như tuyên bố tất cả các structs là "bất biến" (đặt trường của chúng thành readonly) để ngăn ngừa các lỗi có thể xảy ra, hạn chế structtính hữu dụng của s.

C ++, ví dụ, sử dụng một mô hình có thể sử dụng nhiều hơn: nó cho phép bạn tạo một thể hiện đối tượng trên ngăn xếp hoặc trên heap và truyền nó theo giá trị hoặc bằng tham chiếu (hoặc bằng con trỏ). Tôi cứ nghe rằng C # được lấy cảm hứng từ C ++ và tôi không thể hiểu tại sao nó không áp dụng kỹ thuật này. Kết hợp classstructthành một cấu trúc với hai tùy chọn phân bổ khác nhau (heap và stack) và chuyển chúng xung quanh thành các giá trị hoặc (rõ ràng) làm tham chiếu qua refouttừ khóa có vẻ như là một điều tốt đẹp.

Câu hỏi là, tại sao đã classstructtrở thành các khái niệm riêng biệt trong C # và CLR thay vì một loại tổng hợp với hai tùy chọn phân bổ?


34
"Giải pháp được chấp nhận chung cho vấn đề dường như tuyên bố tất cả các cấu trúc là" bất biến "... hạn chế tính hữu dụng của cấu trúc" Nhiều người sẽ cho rằng làm cho bất cứ điều gì bất biến nói chung đều hữu ích hơn bất cứ khi nào nó không phải là nguyên nhân gây ra tắc nghẽn hiệu suất. Ngoài ra, structkhông phải lúc nào cũng được lưu trữ trên ngăn xếp; xem xét một đối tượng với một structlĩnh vực. Điều đó sang một bên, như Mason Wheeler đã đề cập đến vấn đề cắt lát có lẽ là lý do lớn nhất.
Doval

7
Không phải sự thật là C # được lấy cảm hứng từ C ++; thay vì C # được lấy cảm hứng từ tất cả các lỗi (có nghĩa là tốt và nghe tốt vào thời điểm đó) trong thiết kế của cả C ++ và Java.
Pieter Geerkens

18
Lưu ý: stack và heap là chi tiết thực hiện. Không có gì nói rằng các thể hiện cấu trúc phải được phân bổ trên ngăn xếp và các thể hiện lớp phải được phân bổ trên heap. Và nó trong thực tế thậm chí không đúng. Ví dụ, rất có khả năng trình biên dịch có thể xác định bằng cách sử dụng Phân tích thoát rằng một biến không thể thoát khỏi phạm vi cục bộ và do đó phân bổ nó trên ngăn xếp ngay cả khi đó là một thể hiện của lớp. Nó thậm chí không nói rằng phải có một đống hoặc một đống. Bạn có thể phân bổ các khung môi trường dưới dạng một danh sách được liên kết trên heap và thậm chí không có một ngăn xếp nào cả.
Jörg W Mittag


3
"nhưng sau đó bạn tình cờ phát hiện ra một phương thức lấy tham số của loại tổng hợp và để biết liệu nó thực sự thuộc loại giá trị hay loại tham chiếu, bạn phải tìm khai báo của loại đó" Umm, tại sao chính xác lại là vấn đề ? Phương pháp yêu cầu bạn vượt qua một giá trị. Bạn vượt qua một giá trị. Tại thời điểm nào bạn phải quan tâm xem đó là loại tham chiếu hay loại giá trị?
Luaan

Câu trả lời:


58

Lý do C # (và Java và về cơ bản mọi ngôn ngữ OO khác được phát triển sau C ++) đã không sao chép mô hình của C ++ trong khía cạnh này là vì cách C ++ thực hiện nó là một mớ hỗn độn khủng khiếp.

Bạn đã xác định chính xác các điểm có liên quan ở trên :: structloại giá trị, không kế thừa. class: kiểu tham chiếu, có tính kế thừa. Các kiểu kế thừa và giá trị (hay cụ thể hơn là đa hình và truyền qua giá trị) không trộn lẫn; nếu bạn truyền một đối tượng kiểu Derivedcho một đối số phương thức kiểu Base, và sau đó gọi một phương thức ảo trên nó, cách duy nhất để có hành vi đúng là đảm bảo rằng những gì đã vượt qua là một tham chiếu.

Giữa điều đó và tất cả các mớ hỗn độn khác mà bạn gặp phải trong C ++ bằng cách có các đối tượng kế thừa dưới dạng các loại giá trị (sao chép hàm tạo và cắt đối tượng đến với tâm trí!), Giải pháp tốt nhất là Chỉ cần nói Không.

Thiết kế ngôn ngữ tốt không chỉ là triển khai các tính năng, nó còn biết những tính năng nào không nên thực hiện và một trong những cách tốt nhất để thực hiện điều này là học hỏi từ những sai lầm của những người đi trước bạn.


29
Đây chỉ là một lời tán dương chủ quan vô nghĩa khác trên C ++. Không thể downvote, nhưng nếu tôi có thể.
Bartek Banachewicz 27/2/2015

17
@MasonWheeler: " là một mớ hỗn độn khủng khiếp " nghe có vẻ chủ quan. Điều này đã được thảo luận trong một chủ đề bình luận dài cho một câu trả lời khác của bạn ; chủ đề đã bị thu hẹp (thật không may, vì nó chứa những bình luận hữu ích mặc dù trong nước sốt lửa chiến tranh). Tôi không nghĩ rằng nó đáng để lặp lại toàn bộ điều ở đây, nhưng "C # đã hiểu đúng và C ++ đã sai" (dường như đó là thông điệp bạn đang cố gắng truyền tải) thực sự là một tuyên bố chủ quan.
Andy Prowl

15
@MasonWheeler: Tôi đã làm trong chủ đề bị cấm, và một số người khác cũng vậy - đó là lý do tại sao tôi nghĩ rằng thật đáng tiếc khi nó bị xóa. Tôi không nghĩ rằng nên sao chép chủ đề đó ở đây, nhưng phiên bản ngắn là: trong C ++, người dùng loại, không phải người thiết kế , phải quyết định sử dụng loại ngữ nghĩa nào (ngữ nghĩa tham chiếu hoặc giá trị ngữ nghĩa). Điều này có những lợi thế và bất lợi: bạn nổi giận chống lại những khuyết điểm mà không xem xét (hoặc biết?) Ưu điểm. Đó là lý do tại sao phân tích là chủ quan.
Andy Prowl

7
thở dài Tôi tin rằng cuộc thảo luận vi phạm LSP đã diễn ra. Và tôi tin rằng phần lớn mọi người đồng ý rằng đề cập đến LSP là khá kỳ lạ và không liên quan, nhưng không thể kiểm tra vì một mod đã gỡ bỏ luồng nhận xét .
Bartek Banachewicz

9
Nếu bạn di chuyển đoạn cuối cùng của bạn lên đầu và xóa đoạn đầu tiên hiện tại tôi nghĩ bạn có lập luận hoàn hảo. Nhưng đoạn đầu tiên hiện tại chỉ là chủ quan.
Martin York

19

Tương tự, C # về cơ bản giống như một bộ công cụ cơ khí mà ai đó đã đọc rằng bạn thường nên tránh kìm và cờ lê có thể điều chỉnh, do đó, nó không bao gồm cờ lê có thể điều chỉnh được, và kìm bị khóa trong một ngăn kéo đặc biệt được đánh dấu là "không an toàn" và chỉ có thể được sử dụng với sự chấp thuận của người giám sát, sau khi ký từ chối trách nhiệm miễn trừ cho chủ nhân của bạn về bất kỳ trách nhiệm nào đối với sức khỏe của bạn.

C ++, bằng cách so sánh, không chỉ bao gồm cờ lê và kìm có thể điều chỉnh, mà một số công cụ mục đích đặc biệt khá kỳ quặc mà mục đích của chúng không rõ ràng ngay lập tức và nếu bạn không biết cách giữ chúng đúng cách, chúng có thể dễ dàng cắt đứt bạn ngón tay cái (nhưng một khi bạn hiểu cách sử dụng chúng, có thể làm những việc cơ bản là không thể với các công cụ cơ bản trong hộp công cụ C #). Ngoài ra, nó có máy tiện, máy phay, máy mài bề mặt, máy cưa cắt kim loại, v.v., để cho phép bạn thiết kế và tạo ra các công cụ hoàn toàn mới bất cứ khi nào bạn cảm thấy cần (nhưng vâng, những công cụ của thợ máy có thể và sẽ gây ra chấn thương nghiêm trọng nếu bạn không biết bạn đang làm gì với chúng - hoặc thậm chí nếu bạn bất cẩn).

Điều đó phản ánh sự khác biệt cơ bản trong triết lý: C ++ cố gắng cung cấp cho bạn tất cả các công cụ bạn có thể cần cho bất kỳ thiết kế nào bạn muốn. Nó hầu như không cố gắng kiểm soát cách bạn sử dụng các công cụ đó, vì vậy cũng dễ dàng sử dụng chúng để tạo ra các thiết kế chỉ hoạt động tốt trong các tình huống hiếm gặp, cũng như các thiết kế có lẽ chỉ là một ý tưởng tệ hại và không ai biết về tình huống trong đó họ có khả năng làm việc tốt Đặc biệt, rất nhiều điều này được thực hiện bằng cách tách rời các quyết định thiết kế - ngay cả những quyết định trong thực tế gần như luôn luôn được kết hợp. Kết quả là, có một sự khác biệt rất lớn giữa việc chỉ viết C ++ và viết C ++ tốt. Để viết C ++ tốt, bạn cần biết rất nhiều thành ngữ và quy tắc ngón tay cái (bao gồm cả quy tắc ngón tay cái về cách nghiêm túc xem xét lại trước khi phá vỡ các quy tắc khác của ngón tay cái). Kết quả là C ++ được định hướng nhiều hơn về tính dễ sử dụng (của các chuyên gia) hơn là dễ học. Cũng có những trường hợp (tất cả quá nhiều) trong đó nó cũng không thực sự dễ sử dụng.

C # làm nhiều hơn nữa để cố gắng ép buộc (hoặc ít nhất là cực kỳ mạnh mẽ đề xuất) những gì các nhà thiết kế ngôn ngữ coi là thực hành thiết kế tốt. Khá nhiều thứ được tách rời trong C ++ (nhưng thường đi đôi với nhau trong thực tế) được ghép trực tiếp trong C #. Nó không cho phép mã "không an toàn" đẩy ranh giới một chút, nhưng thành thật mà nói, không phải là toàn bộ.

Kết quả là một mặt có khá nhiều thiết kế có thể được thể hiện khá trực tiếp trong C ++, về cơ bản là vụng về hơn để thể hiện trong C #. Mặt khác, đó là một toàn bộ dễ dàng hơn nhiều để tìm hiểu C #, và các cơ hội tạo ra một thiết kế thực sự khủng khiếp đó sẽ không làm việc cho tình hình của bạn (hoặc có thể là bất kỳ khác) đều quyết liệt giảm. Trong nhiều trường hợp (có thể là hầu hết), bạn có thể có được một thiết kế chắc chắn, khả thi bằng cách đơn giản là "đi theo dòng chảy", có thể nói như vậy. Hoặc, là một trong những người bạn của tôi (ít nhất là tôi thích nghĩ về anh ấy như một người bạn - không chắc anh ấy có thực sự đồng ý hay không), C # làm cho nó dễ rơi vào hố thành công.

Vì vậy, nhìn cụ thể hơn vào câu hỏi làm thế nào classstructcó được chúng như thế nào trong hai ngôn ngữ: các đối tượng được tạo trong hệ thống phân cấp thừa kế, nơi bạn có thể sử dụng một đối tượng của lớp dẫn xuất trong vỏ bọc của lớp / giao diện cơ sở của nó, bạn khá nhiều bị mắc kẹt với thực tế là bạn thường cần phải làm như vậy thông qua một số loại con trỏ hoặc tham chiếu - ở mức cụ thể, điều xảy ra là đối tượng của lớp dẫn xuất chứa một bộ nhớ có thể được coi là một thể hiện của lớp cơ sở / giao diện và đối tượng dẫn xuất được thao tác thông qua địa chỉ của phần đó của bộ nhớ.

Trong C ++, tùy thuộc vào lập trình viên để làm điều đó một cách chính xác - khi anh ta sử dụng tính kế thừa, anh ta phải đảm bảo rằng (ví dụ) một hàm hoạt động với các lớp đa hình trong hệ thống phân cấp thực hiện thông qua một con trỏ hoặc tham chiếu đến cơ sở lớp học.

Trong C #, về cơ bản, sự phân tách giống nhau giữa các loại rõ ràng hơn nhiều và được thực thi bởi chính ngôn ngữ. Lập trình viên không cần thực hiện bất kỳ bước nào để vượt qua một thể hiện của một lớp bằng tham chiếu, bởi vì điều đó sẽ xảy ra theo mặc định.


2
Là một người hâm mộ C ++, tôi nghĩ rằng đây là một bản tóm tắt tuyệt vời về sự khác biệt giữa C # và Swiss Army Chainsaw.
David Thornley

1
@DavidThornley: Tôi đã ít nhất cố gắng viết những gì tôi nghĩ sẽ là một so sánh hơi cân bằng. Không chỉ ngón tay, nhưng một số trong những gì tôi thấy khi tôi viết điều này đánh tôi là ... hơi không chính xác (để đặt nó độc đáo).
Jerry Coffin

7

Đây là từ "C #: Tại sao chúng ta cần ngôn ngữ khác?" - Tay súng, Eric:

Đơn giản là một mục tiêu thiết kế quan trọng cho C #.

Có thể vượt qua sự đơn giản và thuần khiết ngôn ngữ nhưng sự thuần khiết vì sự thuần khiết ít được sử dụng cho các lập trình viên chuyên nghiệp. Do đó, chúng tôi đã cố gắng cân bằng mong muốn có một ngôn ngữ đơn giản và ngắn gọn với việc giải quyết các vấn đề trong thế giới thực mà các lập trình viên gặp phải.

[...]

Các loại giá trị , quá tải toán tử và chuyển đổi do người dùng xác định đều làm tăng thêm độ phức tạp cho ngôn ngữ , nhưng cho phép một kịch bản người dùng quan trọng được đơn giản hóa rất nhiều.

Ngữ nghĩa tham chiếu cho các đối tượng là một cách để tránh nhiều rắc rối (tất nhiên và không chỉ cắt đối tượng) mà các vấn đề trong thế giới thực đôi khi có thể yêu cầu các đối tượng có ngữ nghĩa giá trị (ví dụ: Hãy xem Âm thanh như tôi không bao giờ nên sử dụng ngữ nghĩa tham chiếu, phải không? cho một quan điểm khác nhau).

Vì vậy, cách tiếp cận nào tốt hơn để thực hiện hơn là tách biệt những đối tượng bẩn thỉu, xấu xí và xấu với giá trị ngữ nghĩa dưới thẻ của struct?


1
Tôi không biết, có lẽ không sử dụng những đồ vật bẩn thỉu, xấu xí và xấu có liên quan đến ngữ nghĩa?
Bartek Banachewicz

Có lẽ ... tôi là một nguyên nhân lạc lối.
manlio

2
IMHO, một trong những khiếm khuyết lớn nhất trong thiết kế của Java là thiếu phương tiện để tuyên bố liệu một biến đang được sử dụng để đóng gói danh tính hay quyền sở hữu và một trong những khiếm khuyết lớn nhất trong C # là thiếu phương tiện để phân biệt các hoạt động trên một biến từ các hoạt động trên một đối tượng mà một biến giữ một tham chiếu. Ngay cả khi Runtime không quan tâm đến sự khác biệt như vậy, việc có thể chỉ định bằng ngôn ngữ cho dù một biến loại int[]có thể chia sẻ hoặc thay đổi được không (nhưng mảng có thể là một trong hai, nhưng nhìn chung không phải cả hai) sẽ khiến mã sai nhìn sai.
27/2/2015

4

Thay vì nghĩ về các loại giá trị xuất phát từ Object, sẽ hữu ích hơn khi nghĩ về các loại vị trí lưu trữ tồn tại trong một vũ trụ hoàn toàn tách biệt với các loại thể hiện của lớp, nhưng đối với mỗi loại giá trị có một loại đối tượng heap tương ứng. Vị trí lưu trữ của loại cấu trúc chỉ đơn giản là kết hợp các trường công khai và riêng của loại đó và loại heap được tạo tự động theo một mẫu như:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

và cho một câu lệnh như: Console.WriteLine ("Giá trị là {0}", somePoint);

được dịch là: boxed_Point box1 = new boxed_Point (somePoint); Console.WriteLine ("Giá trị là {0}", hộp1);

Trong thực tế, vì các loại vị trí lưu trữ và các loại đối tượng heap tồn tại trong các vũ trụ riêng biệt, không cần thiết phải gọi các loại đối tượng heap như thế nào boxed_Int32; vì hệ thống sẽ biết bối cảnh nào yêu cầu đối tượng heap và đối tượng nào yêu cầu vị trí lưu trữ.

Một số người nghĩ rằng bất kỳ loại giá trị nào không hành xử như đối tượng nên được coi là "xấu xa". Tôi có quan điểm ngược lại: vì vị trí lưu trữ của các loại giá trị không phải là đối tượng cũng không phải là tham chiếu đến các đối tượng, nên mong muốn chúng hoạt động giống như các đối tượng nên được coi là không có ích. Trong trường hợp một cấu trúc có thể hoạt động hữu ích như một đối tượng, không có gì sai khi có một người làm như vậy, nhưng mỗi cấu trúc đều structkhông có gì khác hơn là một tập hợp các lĩnh vực công cộng và riêng tư được dán cùng với băng keo.

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.