Câu trả lời:
Tôi sẽ thêm tiếng nói của mình vào tiếng ồn và cố gắng làm cho mọi thứ rõ ràng:
List<Person> foo = new List<Person>();
và sau đó trình biên dịch sẽ ngăn bạn đưa những thứ không có Person
trong danh sách.
Đằng sau hậu trường, trình biên dịch C # chỉ đưa List<Person>
vào tệp .NET dll, nhưng trong thời gian chạy, trình biên dịch JIT sẽ xây dựng một bộ mã mới, như thể bạn đã viết một lớp danh sách đặc biệt chỉ để chứa người - đại loại như thế ListOfPerson
.
Lợi ích của việc này là nó làm cho nó thực sự nhanh chóng. Không có bất kỳ nội dung nào hoặc bất kỳ nội dung nào khác, và vì dll chứa thông tin rằng đây là Danh sách Person
, mã khác nhìn vào sau này bằng cách sử dụng sự phản chiếu có thể cho biết rằng nó có chứa Person
các đối tượng (vì vậy bạn nhận được thông báo và vân vân).
Nhược điểm của điều này là mã C # 1.0 và 1.1 cũ (trước khi chúng thêm tướng) không hiểu những cái mới này List<something>
, vì vậy bạn phải tự chuyển đổi mọi thứ trở lại cũ List
để tương tác với chúng. Đây không phải là vấn đề lớn, bởi vì mã nhị phân C # 2.0 không tương thích ngược. Lần duy nhất điều này sẽ xảy ra là nếu bạn nâng cấp một số mã C # 1.0 / 1.1 cũ lên C # 2.0
ArrayList<Person> foo = new ArrayList<Person>();
Nhìn bề ngoài, nó trông giống nhau, và nó giống như vậy. Trình biên dịch cũng sẽ ngăn bạn đưa những thứ không có Person
trong danh sách.
Sự khác biệt là những gì xảy ra đằng sau hậu trường. Không giống như C #, Java không đi và xây dựng một thứ đặc biệt ListOfPerson
- nó chỉ sử dụng cái cũ đơn giản ArrayList
luôn có trong Java. Khi bạn lấy mọi thứ ra khỏi mảng, Person p = (Person)foo.get(1);
điệu nhảy đúc thông thường vẫn phải được thực hiện. Trình biên dịch đang tiết kiệm cho bạn các lần bấm phím, nhưng tốc độ nhấn / truyền vẫn còn phát sinh như mọi khi.
Khi mọi người đề cập đến "Loại xóa", đây là những gì họ đang nói về. Trình biên dịch chèn các phôi cho bạn, và sau đó 'xóa' thực tế rằng nó có nghĩa là một danh sách Person
không chỉObject
Lợi ích của phương pháp này là mã cũ không hiểu khái quát không cần phải quan tâm. Nó vẫn xử lý như cũ ArrayList
như mọi khi. Điều này quan trọng hơn trong thế giới java vì họ muốn hỗ trợ biên dịch mã bằng Java 5 với các tổng quát và để nó chạy trên 1.4 hoặc các JVM cũ, mà microsoft đã cố tình quyết định không bận tâm.
Nhược điểm là tốc độ mà tôi đã đề cập trước đây và cũng vì không có ListOfPerson
lớp giả hoặc bất cứ thứ gì giống như đi vào các tệp. Class, mã nhìn vào nó sau này (với sự phản chiếu hoặc nếu bạn rút nó ra khỏi bộ sưu tập khác trong đó nó được chuyển đổi thành Object
hay không) không thể nói theo bất kỳ cách nào nó có nghĩa là một danh sách chỉ chứa Person
và không chỉ bất kỳ danh sách mảng nào khác.
std::list<Person>* foo = new std::list<Person>();
Nó trông giống như chung chung của C # và Java, và nó sẽ làm những gì bạn nghĩ nó nên làm, nhưng đằng sau hậu trường, những điều khác nhau đang xảy ra.
Nó có điểm chung nhất với các tướng C # ở chỗ nó xây dựng đặc biệt pseudo-classes
thay vì chỉ ném thông tin loại như java, nhưng đó là một ấm cá hoàn toàn khác.
Cả C # và Java đều tạo ra đầu ra được thiết kế cho các máy ảo. Nếu bạn viết một số mã có một Person
lớp trong đó, trong cả hai trường hợp, một số thông tin về một Person
lớp sẽ đi vào tệp dll hoặc. Class và JVM / CLR sẽ thực hiện công việc này.
C ++ tạo mã nhị phân x86 thô. Mọi thứ không phải là một đối tượng và không có máy ảo cơ bản nào cần biết về một Person
lớp. Không có quyền anh hoặc unboxing, và các chức năng không phải thuộc về các lớp học, hoặc thực sự bất cứ điều gì.
Do đó, trình biên dịch C ++ không hạn chế những gì bạn có thể làm với các mẫu - về cơ bản bất kỳ mã nào bạn có thể viết thủ công, bạn có thể lấy các mẫu để viết cho bạn.
Ví dụ rõ ràng nhất là thêm những thứ:
Trong C # và Java, hệ thống generic cần biết phương thức nào có sẵn cho một lớp và nó cần truyền lại cho máy ảo. Cách duy nhất để nói với nó điều này là bằng cách mã hóa cứng lớp thực tế hoặc sử dụng các giao diện. Ví dụ:
string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }
Mã đó sẽ không được biên dịch trong C # hoặc Java, vì nó không biết rằng loại T
thực sự cung cấp một phương thức gọi là Tên (). Bạn phải nói với nó - trong C # như thế này:
interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }
Và sau đó, bạn phải đảm bảo những điều bạn vượt qua để addNames thực hiện giao diện IHasName, v.v. Cú pháp java là khác nhau ( <T extends IHasName>
), nhưng nó chịu cùng một vấn đề.
Trường hợp 'cổ điển' cho vấn đề này là cố gắng viết một hàm thực hiện điều này
string addNames<T>( T first, T second ) { return first + second; }
Bạn thực sự không thể viết mã này vì không có cách nào để khai báo một giao diện với +
phương thức trong đó. Bạn thất bại.
C ++ không gặp phải vấn đề nào trong số này. Trình biên dịch không quan tâm đến việc chuyển các loại xuống bất kỳ máy ảo nào - nếu cả hai đối tượng của bạn đều có hàm .Name (), nó sẽ biên dịch. Nếu họ không, nó sẽ không. Đơn giản.
Vì vậy, bạn có nó :-)
int addNames<T>( T first, T second ) { return first + second; }
bằng C #. Kiểu chung có thể được giới hạn trong một lớp thay vì giao diện và có một cách để khai báo một lớp với +
toán tử trong đó.
C ++ hiếm khi sử dụng thuật ngữ khái quát của YouTube. Thay vào đó, các mẫu từ Wikipedia được sử dụng và chính xác hơn. Mẫu mô tả một kỹ thuật để đạt được một thiết kế chung.
Các mẫu C ++ rất khác so với những gì cả C # và Java thực hiện vì hai lý do chính. Lý do đầu tiên là các mẫu C ++ không chỉ cho phép các đối số kiểu thời gian biên dịch mà còn cho các đối số const-value thời gian biên dịch: các mẫu có thể được cung cấp dưới dạng số nguyên hoặc thậm chí cả chữ ký hàm. Điều này có nghĩa là bạn có thể thực hiện một số công cụ khá thú vị vào thời gian biên dịch, ví dụ: tính toán:
template <unsigned int N>
struct product {
static unsigned int const VALUE = N * product<N - 1>::VALUE;
};
template <>
struct product<1> {
static unsigned int const VALUE = 1;
};
// Usage:
unsigned int const p5 = product<5>::VALUE;
Mã này cũng sử dụng tính năng phân biệt khác của các mẫu C ++, cụ thể là chuyên môn hóa mẫu. Mã định nghĩa một mẫu lớp, product
có một đối số giá trị. Nó cũng định nghĩa một chuyên môn hóa cho mẫu đó được sử dụng bất cứ khi nào đối số ước tính thành 1. Điều này cho phép tôi xác định đệ quy trên các định nghĩa mẫu. Tôi tin rằng điều này lần đầu tiên được phát hiện bởi Andrei Alexandrescu .
Chuyên môn hóa mẫu rất quan trọng đối với C ++ vì nó cho phép có sự khác biệt về cấu trúc trong cấu trúc dữ liệu. Các mẫu nói chung là một phương tiện để thống nhất một giao diện giữa các loại. Tuy nhiên, mặc dù điều này là mong muốn, tất cả các loại không thể được đối xử bình đẳng trong quá trình thực hiện. Các mẫu C ++ đưa điều này vào tài khoản. Điều này rất giống với sự khác biệt mà OOP tạo ra giữa giao diện và triển khai với việc ghi đè các phương thức ảo.
Các mẫu C ++ rất cần thiết cho mô hình lập trình thuật toán của nó. Ví dụ, hầu hết tất cả các thuật toán cho vùng chứa được định nghĩa là các hàm chấp nhận loại vùng chứa là kiểu mẫu và xử lý chúng đồng nhất. Trên thực tế, điều đó không hoàn toàn đúng: C ++ không hoạt động trên các container mà thay vào đó là các phạm vi được xác định bởi hai trình vòng lặp, chỉ vào điểm bắt đầu và phía sau điểm cuối của vùng chứa. Do đó, toàn bộ nội dung được các vòng lặp chặn: bắt đầu <= phần tử <end.
Sử dụng các vòng lặp thay vì các thùng chứa là hữu ích vì nó cho phép hoạt động trên các bộ phận của một container thay vì trên toàn bộ.
Một đặc điểm khác biệt của C ++ là khả năng chuyên môn hóa một phần cho các mẫu lớp. Điều này có phần liên quan đến khớp mẫu trên các đối số trong Haskell và các ngôn ngữ chức năng khác. Ví dụ: hãy xem xét một lớp lưu trữ các phần tử:
template <typename T>
class Store { … }; // (1)
Điều này làm việc cho bất kỳ loại yếu tố. Nhưng hãy nói rằng chúng ta có thể lưu trữ con trỏ một cách hiệu quả hơn các loại khác bằng cách áp dụng một số thủ thuật đặc biệt. Chúng tôi có thể làm điều này bằng cách chuyên một phần cho tất cả các loại con trỏ:
template <typename T>
class Store<T*> { … }; // (2)
Bây giờ, bất cứ khi nào chúng ta ví dụ một mẫu container cho một loại, định nghĩa phù hợp sẽ được sử dụng:
Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.
Bản thân Anders Hejlsberg đã mô tả sự khác biệt ở đây " Generics in C #, Java và C ++ ".
Đã có rất nhiều câu trả lời tốt về những gì sự khác biệt là, vì vậy hãy để tôi đưa ra một quan điểm hơi khác nhau và thêm lý do tại sao .
Như đã được giải thích, sự khác biệt chính là xóa kiểu , tức là trình biên dịch Java xóa các kiểu chung và chúng không kết thúc trong mã byte được tạo. Tuy nhiên, câu hỏi là: tại sao mọi người sẽ làm điều đó? Nó không có ý nghĩa! Hay không?
Chà, cái gì thay thế? Nếu bạn không thực hiện Generics trong ngôn ngữ, nơi làm bạn thực hiện chúng? Và câu trả lời là: trong Máy ảo. Mà phá vỡ khả năng tương thích ngược.
Kiểu xóa, mặt khác, cho phép bạn trộn các máy khách chung với các thư viện không chung chung. Nói cách khác: mã được biên dịch trên Java 5 vẫn có thể được triển khai thành Java 1.4.
Microsoft, tuy nhiên, đã quyết định phá vỡ tính tương thích ngược cho thuốc generic. Đó là lý do tại sao .NET Generics "tốt" hơn Java Generics.
Tất nhiên, Sun không phải là kẻ ngốc hay hèn nhát. Lý do tại sao họ "bực mình", là vì Java đã cũ hơn và phổ biến hơn so với .NET khi họ giới thiệu chung chung. (Chúng được giới thiệu gần như cùng một lúc ở cả hai thế giới.) Phá vỡ khả năng tương thích ngược sẽ là một nỗi đau rất lớn.
Nói cách khác: trong Java, Generics là một phần của Ngôn ngữ (có nghĩa là chúng chỉ áp dụng cho Java, không áp dụng cho các ngôn ngữ khác), trong .NET chúng là một phần của Máy ảo (có nghĩa là chúng áp dụng cho tất cả các ngôn ngữ, không phải chỉ C # và Visual Basic.NET).
So sánh điều này với các tính năng .NET như LINQ, biểu thức lambda, suy luận kiểu biến cục bộ, kiểu ẩn danh và cây biểu thức: đây là tất cả các tính năng ngôn ngữ . Đó là lý do tại sao có sự khác biệt tinh tế giữa VB.NET và C #: nếu các tính năng đó là một phần của VM, thì chúng sẽ giống nhau trong tất cả các ngôn ngữ. Nhưng CLR không thay đổi: nó vẫn giống với .NET 3.5 SP1 như trong .NET 2.0. Bạn có thể biên dịch chương trình C # sử dụng LINQ với trình biên dịch .NET 3.5 và vẫn chạy nó trên .NET 2.0, miễn là bạn không sử dụng bất kỳ thư viện .NET 3.5 nào. Điều đó sẽ không hoạt động với generic và .NET 1.1, nhưng nó sẽ hoạt động với Java và Java 1.4.
ArrayList<T>
có thể được phát ra dưới dạng một kiểu mới được đặt tên nội bộ với Class<T>
trường tĩnh (ẩn) . Miễn là phiên bản mới của lib chung đã được triển khai với mã byte 1,5+, nó sẽ có thể chạy trên 1,4- JVM.
Theo dõi bài viết trước của tôi.
Các mẫu là một trong những lý do chính khiến C ++ thất bại rất nhiều tại intellisense, bất kể IDE được sử dụng. Do chuyên môn hóa mẫu, IDE không bao giờ có thể thực sự chắc chắn nếu một thành viên nhất định tồn tại hay không. Xem xét:
template <typename T>
struct X {
void foo() { }
};
template <>
struct X<int> { };
typedef int my_int_type;
X<my_int_type> a;
a.|
Bây giờ, con trỏ đang ở vị trí được chỉ định và thật khó để IDE có thể nói tại thời điểm đó nếu và những gì, các thành viên a
có. Đối với các ngôn ngữ khác, việc phân tích cú pháp sẽ đơn giản nhưng đối với C ++, cần có khá nhiều đánh giá trước đó.
Nó trở nên tồi tệ hơn. Điều gì nếu my_int_type
được định nghĩa bên trong một mẫu lớp? Bây giờ loại của nó sẽ phụ thuộc vào đối số loại khác. Và ở đây, ngay cả trình biên dịch thất bại.
template <typename T>
struct Y {
typedef T my_type;
};
X<Y<int>::my_type> b;
Sau một chút suy nghĩ, một lập trình viên sẽ kết luận rằng mã này giống như trên: Y<int>::my_type
giải quyết int
, do đó b
nên cùng loại với a
, phải không?
Sai lầm. Tại thời điểm trình biên dịch cố gắng giải quyết câu lệnh này, nó thực sự Y<int>::my_type
chưa biết ! Vì vậy, nó không biết rằng đây là một loại. Nó có thể là một cái gì đó khác, ví dụ như một hàm thành viên hoặc một trường. Điều này có thể làm phát sinh sự mơ hồ (mặc dù không phải trong trường hợp hiện tại), do đó trình biên dịch thất bại. Chúng ta phải nói rõ rằng chúng ta đề cập đến một tên loại:
X<typename Y<int>::my_type> b;
Bây giờ, mã biên dịch. Để xem sự mơ hồ phát sinh từ tình huống này như thế nào, hãy xem xét đoạn mã sau:
Y<int>::my_type(123);
Câu lệnh mã này hoàn toàn hợp lệ và yêu cầu C ++ thực hiện lệnh gọi hàm Y<int>::my_type
. Tuy nhiên, nếu my_type
không phải là một hàm mà là một kiểu, câu lệnh này vẫn có hiệu lực và thực hiện một kiểu đúc đặc biệt (kiểu kiểu hàm) thường là một lời gọi của hàm tạo. Trình biên dịch không thể cho biết ý nghĩa của chúng tôi vì vậy chúng tôi phải định hướng ở đây.
Cả Java và C # đều giới thiệu thuốc generic sau khi phát hành ngôn ngữ đầu tiên. Tuy nhiên, có những khác biệt trong cách các thư viện cốt lõi thay đổi khi thuốc generic được giới thiệu. C # 's Generics không chỉ biên dịch ma thuật và vì vậy nó đã không thể generify lớp thư viện hiện có mà không phá vỡ tính tương thích ngược.
Ví dụ, trong Java, Bộ sưu tập Khung hiện tại đã hoàn toàn chung chung . Java không có cả phiên bản chung và không chung của các lớp bộ sưu tập. Theo một số cách, điều này sạch sẽ hơn nhiều - nếu bạn cần sử dụng một bộ sưu tập trong C # thì thực sự có rất ít lý do để đi với phiên bản không chung chung, nhưng các lớp kế thừa đó vẫn được giữ nguyên, làm lộn xộn cảnh quan.
Một sự khác biệt đáng chú ý khác là các lớp Enum trong Java và C #. Enum của Java có định nghĩa trông hơi khó hiểu này:
// java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
(xem phần giải thích rất rõ ràng của Angelika Langer về chính xác lý do tại sao lại như vậy. Về cơ bản, điều này có nghĩa là Java có thể cấp quyền truy cập an toàn từ một chuỗi đến giá trị Enum của nó:
// Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");
So sánh phiên bản này với phiên bản của C #:
// Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");
Vì Enum đã tồn tại trong C # trước khi thuốc generic được đưa vào ngôn ngữ, định nghĩa không thể thay đổi nếu không phá vỡ mã hiện có. Vì vậy, giống như các bộ sưu tập, nó vẫn nằm trong các thư viện cốt lõi ở trạng thái cũ.
ArrayList
thành List<T>
và đặt nó vào một không gian tên mới. Thực tế là, nếu có một lớp xuất hiện trong mã nguồn vì ArrayList<T>
nó sẽ trở thành một tên lớp trình biên dịch khác được tạo trong mã IL, do đó không có xung đột tên nào có thể xảy ra.
Trễ 11 tháng, nhưng tôi nghĩ câu hỏi này đã sẵn sàng cho một số nội dung Java Wildcard.
Đây là một tính năng cú pháp của Java. Giả sử bạn có một phương pháp:
public <T> void Foo(Collection<T> thing)
Và giả sử bạn không cần phải tham khảo kiểu T trong thân phương thức. Bạn đang khai báo tên T và sau đó chỉ sử dụng một lần, vậy tại sao bạn phải nghĩ ra tên cho nó? Thay vào đó, bạn có thể viết:
public void Foo(Collection<?> thing)
Dấu hỏi yêu cầu trình biên dịch giả vờ rằng bạn đã khai báo một tham số loại có tên bình thường chỉ cần xuất hiện một lần ở vị trí đó.
Bạn không thể làm gì với các ký tự đại diện mà bạn cũng không thể làm với tham số loại được đặt tên (đó là cách những điều này luôn được thực hiện trong C ++ và C #).
class Foo<T extends List<?>>
và sử dụng Foo<StringList>
nhưng trong C #, bạn phải thêm tham số loại bổ sung đó: class Foo<T, T2> where T : IList<T2>
và sử dụng clunky Foo<StringList, String>
.
Wikipedia có các bài viết tuyệt vời so sánh cả các mẫu Java / C # generic và Java Generics / C ++ . Các bài viết chính trên Generics có vẻ hơi lộn xộn nhưng nó có một số thông tin tốt trong đó.
Khiếu nại lớn nhất là loại tẩy xóa. Trong đó, thuốc generic không được thi hành trong thời gian chạy. Đây là một liên kết đến một số tài liệu Sun về chủ đề này .
Generics được thực hiện theo kiểu xóa: thông tin kiểu chung chỉ xuất hiện tại thời điểm biên dịch, sau đó nó bị trình biên dịch xóa.
Các mẫu C ++ thực sự mạnh hơn nhiều so với các đối tác C # và Java của chúng khi chúng được đánh giá tại thời điểm biên dịch và hỗ trợ chuyên môn hóa. Điều này cho phép Lập trình siêu mẫu và làm cho trình biên dịch C ++ tương đương với máy Turing (tức là trong quá trình biên dịch, bạn có thể tính toán bất cứ thứ gì có thể tính toán được với máy Turing).
Có vẻ như, trong số các đề xuất rất thú vị khác, có một đề xuất về tinh chỉnh tổng quát và phá vỡ tính tương thích ngược:
Hiện tại, generic được thực hiện bằng cách sử dụng erasure, có nghĩa là thông tin loại chung không có sẵn trong thời gian chạy, điều này làm cho một số loại mã khó viết. Generics đã được thực hiện theo cách này để hỗ trợ khả năng tương thích ngược với mã không chung chung cũ. Tổng quát hóa sẽ làm cho thông tin loại chung có sẵn trong thời gian chạy, sẽ phá vỡ mã không chung chung. Tuy nhiên, Neal Gafter đã đề xuất các loại chỉ có thể hiển thị lại nếu được chỉ định, để không phá vỡ tính tương thích ngược.
Lưu ý: Tôi không có đủ điểm để nhận xét, vì vậy hãy thoải mái chuyển nhận xét này thành nhận xét để trả lời thích hợp.
Trái với niềm tin phổ biến, mà tôi không bao giờ hiểu nó đến từ đâu, .net đã triển khai các khái quát thực sự mà không phá vỡ tính tương thích ngược và họ đã dành nỗ lực rõ ràng cho điều đó. Bạn không phải thay đổi mã .net 1.0 không chung chung thành chung chung để được sử dụng trong .net 2.0. Cả danh sách chung và không chung chung vẫn có sẵn trong .Net framework 2.0 ngay cả cho đến 4.0, chính xác không có gì khác ngoài lý do tương thích ngược. Do đó, các mã cũ vẫn sử dụng ArrayList không chung chung vẫn sẽ hoạt động và sử dụng cùng một lớp ArrayList như trước đây. Khả năng tương thích mã ngược luôn được duy trì từ 1.0 đến giờ ... Vì vậy, ngay cả trong .net 4.0, bạn vẫn phải tùy chọn sử dụng bất kỳ lớp không chung chung nào từ 1.0 BCL nếu bạn chọn làm như vậy.
Vì vậy, tôi không nghĩ java phải phá vỡ tính tương thích ngược để hỗ trợ các tổng quát thực sự.
ArrayList<Foo>
mà nó muốn chuyển sang một phương thức cũ hơn được cho là để đưa vào một ArrayList
thể hiện của Foo
. Nếu ArrayList<foo>
không phải là một ArrayList
, làm thế nào để làm cho nó hoạt động?