Tính đa hình trong Java. Liên kết động và tĩnh

Đoạn này, mặc dù ngắn gọn, nhưng rất quan trọng - hầu như tất cả đều chuyên nghiệp lập trình trong Java dựa trên việc sử dụng tính đa hình. Đồng thời, chủ đề này là một trong những chủ đề khó hiểu nhất đối với học sinh. Do đó, bạn nên đọc kỹ đoạn này nhiều lần.

Các phương thức lớp được đánh dấu bằng công cụ sửa đổi tĩnh không phải do ngẫu nhiên - đối với chúng, khi biên dịch mã chương trình, liên kết tĩnh. Điều này có nghĩa là trong ngữ cảnh tên phương thức được chỉ định trong mã nguồn của lớp nào, thì tham chiếu được thực hiện đến phương thức của lớp đó trong mã đã biên dịch. Đó là, tiến hành ràng buộc tên phương thức tại địa điểm cuộc gọi với mã thực thi phương pháp này. Đôi khi liên kết tĩnh triệu tập ràng buộc sớm, vì nó xảy ra ở giai đoạn biên dịch của chương trình. Liên kết tĩnh trong Java nó được sử dụng trong một trường hợp nữa - khi lớp được khai báo bằng bổ ngữ cuối cùng ("final", "final").

Các phương thức đối tượng trong Java là động, nghĩa là, chúng tuân theo ràng buộc động. Nó xảy ra ở giai đoạn thực thi chương trình trực tiếp trong khi gọi phương thức, và ở giai đoạn viết phương thức này, người ta không biết trước lời gọi sẽ được thực hiện từ lớp nào. Điều này được xác định bởi loại đối tượng mà mã này đang làm việc - đối tượng thuộc về lớp nào, từ lớp đó phương thức được gọi. Liên kết này xảy ra muộn hơn nhiều so với mã phương thức được biên dịch. Do đó, kiểu ràng buộc này thường được gọi là Ràng buộc muộn.

Mã dựa trên cuộc gọi phương pháp động, có tài sản đa hình- cùng một đoạn mã hoạt động khác nhau tùy thuộc vào kiểu đối tượng gọi nó là gì, nhưng thực hiện những việc giống nhau ở mức độ trừu tượng liên quan đến mã nguồn của phương thức.

Để làm rõ những từ không rõ ràng này trong lần đọc đầu tiên, hãy xem xét ví dụ từ đoạn trước - hoạt động của phương thức moveTo. Đối với các lập trình viên thiếu kinh nghiệm, có vẻ như phương thức này nên được ghi đè trong mọi lớp dẫn xuất. Nó thực sự có thể được thực hiện, và mọi thứ sẽ hoạt động chính xác. Nhưng mã như vậy sẽ cực kỳ thừa - sau cùng, việc triển khai phương thức sẽ nằm trong tất cả các lớp con con số giống hệt nhau:

public void moveTo (int x, int y) (hide (); this.x = x; this.y = y; show (););

Ngoài ra, nó không tận dụng được tính đa hình. Vì vậy, chúng tôi sẽ không làm điều đó.

Người ta thường phân vân tại sao lớp trừu tượng con số viết một triển khai của phương pháp này. Rốt cuộc, các lệnh gọi phương thức ẩn và hiện được sử dụng trong đó, thoạt nhìn, phải là các lệnh gọi phương pháp trừu tượng- đó là, có vẻ như họ không thể làm việc ở tất cả!

Nhưng các phương thức ẩn và hiện là động, như chúng ta đã biết, có nghĩa là việc ràng buộc tên phương thức và mã thực thi của nó được thực hiện trong thời gian chạy. Do đó, thực tế là các phương thức này được chỉ định trong ngữ cảnh của lớp con số, hoàn toàn không có nghĩa là họ sẽ được gọi từ lớp con số! Hơn nữa, bạn có thể đảm bảo rằng các phương thức ẩn và hiện sẽ không bao giờ được gọi từ lớp này. Giả sử chúng ta có các biến dot1 kiểu Dot và circle1 kiểu Circle, và chúng được gán các tham chiếu đối tượng của các kiểu tương ứng. Hãy xem các lệnh gọi dot1.moveTo (x1, y1) và circle1.moveTo (x2, y2) hoạt động như thế nào.

Khi dot1.moveTo (x1, y1) được gọi, một cuộc gọi được thực hiện từ lớp con số phương thức moveTo. Thật vậy, phương thức này trong lớp Dot không bị ghi đè, có nghĩa là nó được kế thừa từ con số. Trong phương thức moveTo, câu lệnh đầu tiên là lời gọi phương thức dynamic hide. Việc triển khai phương thức này được lấy từ lớp mà đối tượng dot1 gọi phương thức này là một thể hiện. Đó là, từ lớp Chấm. Vì vậy, điểm được ẩn. Sau đó, có một sự thay đổi trong tọa độ của đối tượng, sau đó nó được gọi là phương pháp động buổi bieu diễn. Việc triển khai phương thức này được lấy từ lớp mà đối tượng dot1 gọi phương thức này là một thể hiện. Đó là, từ lớp Chấm. Do đó, một điểm được hiển thị tại vị trí mới.

Để gọi circle1.moveTo (x2, y2) mọi thứ hoàn toàn giống nhau - các phương thức động ẩn và hiện được gọi từ lớp mà đối tượng circle1 là một thể hiện, tức là từ lớp Circle. Do đó, hình tròn bị ẩn ở vị trí cũ và hiển thị ở vị trí mới.

Nghĩa là, nếu đối tượng là một điểm, thì điểm đó sẽ di chuyển. Và nếu đối tượng là một hình tròn, thì hình tròn chuyển động. Hơn nữa, nếu một ngày nào đó ai đó viết, chẳng hạn như một lớp Ellipse kế thừa từ Circle và tạo một đối tượng Ellipse ellipse = new Ellipse (…), sau đó gọi ellipse.moveTo (…) sẽ di chuyển đến một vị trí mới trên ellipse. Và điều này sẽ xảy ra theo cách thực hiện các phương thức ẩn và hiện trong lớp Ellipse. Lưu ý rằng mã đa hình đã biên dịch của lớp sẽ hoạt động từ lâu con số. Tính đa hình được đảm bảo bởi thực tế là các tham chiếu đến các phương thức này không được đưa vào mã của phương thức moveTo tại thời điểm biên dịch - chúng được đặt thành các phương thức có tên như vậy từ lớp của đối tượng đang gọi ngay tại thời điểm phương thức moveTo là triệu tập.

Trong ngôn ngữ lập trình hướng đối tượng, có hai loại phương pháp động- thực sự năng động và ảo. Theo nguyên lý hoạt động thì chúng hoàn toàn giống nhau và chỉ khác nhau về tính năng thực hiện. Gọi phương pháp ảo nhanh hơn. Gọi động chậm hơn nhưng bảng dịch vụ phương pháp động(DMT - Bảng phương pháp động) chiếm ít bộ nhớ hơn một chút so với bảng phương pháp ảo(VMT - Bảng phương thức ảo).

Nó có vẻ giống như một thách thức phương pháp động thời gian không hiệu quả do tra cứu tên dài. Trên thực tế, trong quá trình gọi, không có tra cứu tên nào được thực hiện, nhưng một cơ chế nhanh hơn được sử dụng, sử dụng bảng các phương thức ảo (động) đã đề cập. Nhưng chúng tôi sẽ không tập trung vào các tính năng triển khai của các bảng này, vì Java không phân biệt giữa các loại phương thức này.

6,8. Đối tượng lớp cơ sở

Lớp Đối tượng là lớp cơ sở cho tất cả các lớp Java. Do đó, tất cả các trường và phương thức của nó được kế thừa và chứa trong tất cả các lớp. Lớp Đối tượng chứa các phương thức sau:

  • public Boolean bằng (Object obj)- trả về true nếu các giá trị của đối tượng mà từ đó phương thức được gọi và đối tượng được truyền qua tham chiếu obj trong danh sách tham số là bằng nhau. Nếu các đối tượng không bằng nhau, giá trị false được trả về. Trong lớp Object, bình đẳng được coi là bình đẳng tham chiếu và tương đương với toán tử so sánh "==". Nhưng trong các phần tử con, phương thức này có thể được ghi đè và có thể so sánh các đối tượng theo nội dung của chúng. Ví dụ, điều này xảy ra đối với các đối tượng lớp số của trình bao bọc. Điều này rất dễ dàng để kiểm tra với mã này:

    Đôi d1 = 1,0, d2 = 1,0; System.out.println ("d1 == d2 =" + (d1 == d2)); System.out.println ("d1.equals (d2) =" + (d1.equals (d2)));

    Dòng đầu tiên của đầu ra sẽ cho d1 == d2 = false và d1 thứ hai. bằng (d2) = true

  • public int hashCode ()- vấn đề Mã Băm sự vật. Mã băm là một định danh số duy nhất có điều kiện được liên kết với một phần tử. Vì lý do bảo mật, bạn không thể cung cấp địa chỉ của một đối tượng cho một chương trình ứng dụng. Do đó, trong Java, mã băm thay thế địa chỉ của một đối tượng trong trường hợp các bảng địa chỉ đối tượng phải được lưu trữ cho một số mục đích.
  • Bản sao đối tượng được bảo vệ () ném CloneNotSupportedException - phương thức đang sao chép đối tượng và trả về một liên kết đến bản sao đã tạo (bản sao) của đối tượng. Trong những người thừa kế của lớp Đối tượng, nó phải được ghi đè và cũng chỉ ra rằng lớp thực hiện giao diện Clonable. Cố gắng gọi một phương thức từ một đối tượng không hỗ trợ nguyên nhân nhân bản ném một ngoại lệ CloneNotSupportedException ("Không hỗ trợ sao chép"). Các giao diện và ngoại lệ sẽ được thảo luận sau.

    Có hai loại sao chép: nông (cạn), khi các giá trị của các trường của đối tượng gốc được sao chép lần lượt vào bản sao và sâu (sâu), trong đó các đối tượng mới được tạo cho các trường của kiểu tham chiếu, sao chép các đối tượng được tham chiếu bởi các trường của bản gốc. Với nhân bản nông, cả bản gốc và bản sao sẽ tham chiếu đến các đối tượng giống nhau. Nếu đối tượng chỉ có các trường các loại nguyên thủy, không có sự khác biệt giữa nhân bản nông và sâu. Nhân bản được thực hiện bởi lập trình viên phát triển lớp; không có cơ chế nhân bản tự động. Và đó là ở giai đoạn phát triển lớp, bạn nên quyết định lựa chọn tùy chọn nhân bản nào. Trong phần lớn các trường hợp, nó được yêu cầu nhân bản sâu.

  • public Final Class getClass ()- trả về một tham chiếu đến một siêu đối tượng của loại lớp. Với nó, bạn có thể lấy thông tin về lớp mà một đối tượng thuộc về và gọi các phương thức lớp và trường lớp của nó.
  • void finalize () được bảo vệ ném Throwable - được gọi trước khi đối tượng bị phá hủy. Nên ghi đè trong những con cháu của Đối tượng cần thực hiện một số thao tác bổ trợ trước khi phá hủy đối tượng (đóng tệp, hiển thị thông báo, vẽ thứ gì đó trên màn hình, v.v.). Thông tin thêm về phương pháp này được mô tả trong đoạn tương ứng.
  • public String toString ()- trả về một biểu diễn chuỗi của đối tượng (càng đầy đủ càng tốt). Trong lớp Đối tượng, phương thức này in tên đủ điều kiện của đối tượng (với tên gói) thành một chuỗi, theo sau là ký tự "@", sau đó là mã băm của đối tượng ở dạng thập lục phân. Hầu hết các lớp tiêu chuẩn ghi đè phương thức này. Đối với các lớp số, biểu diễn chuỗi của số được trả về, đối với các lớp chuỗi, nội dung của chuỗi, đối với các lớp ký tự, chính ký tự đó (chứ không phải biểu diễn chuỗi mã của nó!). Ví dụ: đoạn mã sau

    Object obj = new Object (); System.out.println ("obj.toString () cho" + obj.toString ()); Double d = new Double (1,0); System.out.println ("d.toString () cho" + d.toString ()); Kí tự c = "A"; System.out.println ("c.toString () cho" + c.toString ());

    sẽ cung cấp một kết luận

    obj.toString () cho [email được bảo vệ] d.toString () cho 1,0 c.toString () cho A

Ngoài ra còn có các phương pháp thông báo(), tifyAll () và một số biến thể được nạp chồng của phương thức đợi đã, được thiết kế để làm việc với các chủ đề. Chúng được thảo luận trong phần về chủ đề.

6,9. Người xây dựng. Các từ dành riêng siêu và điều này. Khối khởi tạo

Như đã đề cập, các đối tượng trong Java được tạo bằng cách sử dụng từ dành riêng mới, sau đó là hàm khởi tạo - một chương trình con đặc biệt tạo một đối tượng và khởi tạo các trường của đối tượng được tạo. Nó không chỉ định kiểu trả về và nó không phải là phương thức đối tượng (được gọi thông qua tên lớp khi đối tượng chưa tồn tại), cũng không phải phương thức lớp (đối tượng và các trường của nó có sẵn trong phương thức khởi tạo thông qua tham chiếu này) . Trên thực tế, hàm tạo, kết hợp với toán tử new, trả về một tham chiếu đến đối tượng đang được tạo và có thể được coi là một loại phương thức đặc biệt kết hợp các tính năng của phương thức lớp và phương thức đối tượng.

Nếu đối tượng không yêu cầu bất kỳ khởi tạo bổ sung nào khi nó được tạo, bạn có thể sử dụng phương thức khởi tạo có sẵn theo mặc định cho mọi lớp. Đây là tên của lớp được theo sau bởi dấu ngoặc đơn trống - không có danh sách tham số. Một phương thức khởi tạo như vậy không cần phải được chỉ định khi phát triển một lớp, nó tự động hiện diện.

Nếu khởi tạo là bắt buộc, các hàm tạo với danh sách các tham số thường được sử dụng. Chúng ta đã thấy các ví dụ về các hàm tạo như vậy cho các lớp Chấm và Vòng tròn. Các lớp Chấm và Vòng tròn được kế thừa từ các lớp trừu tượng, không có hàm tạo. Nếu có sự kế thừa từ một lớp không trừu tượng, tức là một lớp đã có sẵn một phương thức khởi tạo (ngay cả khi nó là một phương thức khởi tạo mặc định), thì một số chi tiết cụ thể sẽ phát sinh. Câu lệnh đầu tiên trong một phương thức khởi tạo phải là một lời gọi đến phương thức khởi tạo lớp cha. Nhưng nó được thực hiện không phải thông qua tên của lớp này, mà với sự trợ giúp của một từ dành riêng siêu(từ "superclass"), theo sau là danh sách các tham số cần thiết cho hàm tạo mẹ. Hàm khởi tạo này khởi tạo các trường dữ liệu được kế thừa từ lớp cha (bao gồm tất cả các ông bà trước đó). Ví dụ, hãy viết lớp FilledCircle - một lớp kế thừa của Circle, một thể hiện của lớp này sẽ được vẽ dưới dạng một vòng tròn màu.

gói java_gui_example; nhập java.awt. *; public class FilledCircle mở rộng Circle (/ ** Tạo một phiên bản mới của FilledCircle * / public FilledCircle (Graphics g, Color bgColor, int r, Color color) (super (g, bgColor, r); this.color = color;) public void show () (Color oldC = Graphics.getColor (); graphics.setColor (color); graphics.setXORMode (bgColor); graphics.fillOval (x, y, size, size); graphics.setColor (oldC); đồ họa. setPaintMode ();) public void hide () (Color oldC = Graphics.getColor (); Graphics.setColor (color); Graphics.setXORMode (bgColor); Graphics.fillOval (x, y, size, size); Graphics.setColor (oldC); graphics.setPaintMode ();))

Nói chung, logic của việc tạo các đối tượng phức tạp: phần cha của đối tượng được tạo và khởi tạo đầu tiên, bắt đầu từ phần được kế thừa từ lớp Object và xa hơn nữa dọc theo hệ thống phân cấp, kết thúc bằng phần liên quan đến chính lớp đó. Đó là lý do tại sao câu lệnh đầu tiên của một hàm tạo thường là một lệnh gọi đến hàm tạo cấp cao ( danh sách tham số), vì việc truy cập vào phần chưa được khởi tạo của đối tượng được duy trì bởi lớp cha có thể dẫn đến những hậu quả không thể đoán trước.

Ở lớp này, chúng tôi sử dụng cách vẽ và "ẩn" hình nâng cao hơn so với các lớp trước. Nó dựa trên việc sử dụng chế độ vẽ XOR ("độc quyền hoặc"). Chế độ này được đặt bằng phương pháp setXORMode. Trong trường hợp này, việc vẽ hình lặp đi lặp lại đến cùng một vị trí dẫn đến việc khôi phục hình ảnh gốc trong vùng đầu ra. Quá trình chuyển đổi sang chế độ vẽ bình thường được thực hiện bằng phương thức setPaintMode.

Thường được sử dụng trong các hàm tạo

2

Giả sử không có hàm hello và về cơ bản chúng ta chỉ gọi ob.display, sau đó nó gọi hàm hiển thị của lớp B, không phải lớp A.

Lệnh gọi hàm display () được trình biên dịch đặt một lần thành phiên bản được định nghĩa trong lớp cơ sở. Đây được gọi là giải pháp lệnh gọi hàm tĩnh hoặc ràng buộc tĩnh - lệnh gọi hàm được cố định trước khi chương trình được thực thi. Điều này đôi khi cũng được gọi là liên kết sớm vì hàm display () được đặt tại thời điểm biên dịch.

Bây giờ, làm thế nào nó có thể gọi hàm hiển thị của lớp dẫn xuất mà không sử dụng từ khóa ảo (liên kết muộn) trước hàm hiển thị trong lớp cơ sở?

Bây giờ trong chương trình này, việc chuyển đối tượng dưới dạng gọi theo giá trị, gọi bằng con trỏ và gọi bằng tham chiếu đến hàm Hello hoạt động tốt. Bây giờ, nếu chúng ta đang sử dụng Đa hình và chúng ta muốn ánh xạ một hàm thành viên của một lớp dẫn xuất nếu nó được gọi, chúng ta phải thêm từ khóa ảo trước hàm ánh xạ trong cơ sở. Nếu bạn truyền giá trị của một đối tượng khi được gọi bằng con trỏ và gọi bằng tham chiếu thì đó là một lệnh gọi hàm trong lớp dẫn xuất, nhưng nếu bạn truyền một đối tượng theo giá trị thì không phải tại sao lại như vậy?>

Lớp A (public: void display (); // virtual void display () (cout<< "Hey from A" <display ()) int main () (B obj; Hello (obj); // obj // & ob return 0;)

  • 2 câu trả lời
  • Sắp xếp:

    Hoạt động

4

bây giờ làm thế nào nó có thể gọi hàm ánh xạ của lớp dẫn xuất mà không sử dụng từ khóa virtual (late binding) trước hàm ánh xạ trong lớp cơ sở?

Một hàm không phải ảo được trình biên dịch giải quyết đơn giản theo kiểu tĩnh của đối tượng (hoặc tham chiếu hoặc con trỏ) mà nó gọi. Do đó, đối tượng đã cho của kiểu dẫn xuất, cũng như một tham chiếu đến subobject của nó:

Bb; Đ & a = b;

bạn sẽ nhận được các kết quả khác khi gọi một hàm không phải ảo:

b.display (); // được gọi là B a.display (); // được gọi là A

Nếu bạn biết loại thực, thì bạn có thể chỉ định những gì bạn muốn gọi là phiên bản này:

static_cast (a) .display (); // được gọi là B

nhưng điều gì sẽ là sai lầm khủng khiếp nếu đối tượng mà a đề cập đến không thuộc loại B.

Bây giờ, nếu chúng ta đang sử dụng Đa hình và chúng ta muốn ánh xạ một hàm thành viên của một lớp dẫn xuất nếu nó được gọi, chúng ta phải thêm một từ khóa ảo trước hàm ánh xạ trong cơ sở.

Để sửa. Nếu bạn đặt một hàm là ảo, thì nó sẽ được giải quyết tại thời điểm chạy theo kiểu động của đối tượng, ngay cả khi bạn sử dụng một loại tham chiếu hoặc con trỏ khác để truy cập nó. Vì vậy, cả hai ví dụ trên sẽ gọi nó là B.

Nếu chúng ta truyền giá trị của một đối tượng bằng cách gọi bằng con trỏ và gọi bằng tham chiếu, nó sẽ gọi hàm trong lớp dẫn xuất, nhưng nếu chúng ta truyền đối tượng theo giá trị, điều đó không có nghĩa là tại sao lại như vậy?

Nếu bạn chuyển nó theo giá trị, thì bạn vết cắt nó: chỉ sao chép phần A của đối tượng để tạo một đối tượng mới kiểu A. Vì vậy, cho dù hàm đó có phải là ảo hay không, việc gọi nó trên đối tượng đó sẽ chọn phiên bản của A, vì nó là A và không có gì khác ngoài A.

0

wikipedia nói rằng việc cắt đối tượng xảy ra bởi vì không có nơi nào để lưu trữ các thành viên bổ sung của lớp dẫn xuất trong lớp cha, vì vậy nó đang cắt. Tại sao việc cắt các đối tượng không xảy ra nếu chúng ta truyền nó bằng tham chiếu hoặc con trỏ? Tại sao lớp cha có thêm không gian để lưu trữ nó? -

Ràng buộc muộn Với Thành phần COM

Trước khi máy khách thực thi có thể gọi các phương thức và thuộc tính của đối tượng bean, nó cần biết địa chỉ bộ nhớ của các phương thức và thuộc tính đó. Có hai công nghệ khác nhau mà các chương trình khách hàng có thể sử dụng để xác định các địa chỉ này.

Các chương trình liên kết sớm tìm hiểu địa chỉ sớm trong quá trình biên dịch / thực thi, tại thời điểm biên dịch. Khi một chương trình được biên dịch bằng cách liên kết sớm, trình biên dịch sử dụng thư viện kiểu của thành phần để bao gồm địa chỉ của các phương thức và thuộc tính của thành phần trong tệp thực thi của máy khách để các địa chỉ có thể được truy cập rất nhanh và không bị lỗi. đã được thảo luận cho đến nay sử dụng ràng buộc sớm.

Về phần mình, các chương trình bị ràng buộc muộn học địa chỉ thuộc tính và phương thức muộn trong quá trình biên dịch / thực thi, tại chính thời điểm khi các thuộc tính và phương thức này được gọi. Mã giới hạn muộn thường truy cập các đối tượng khách thông qua các kiểu dữ liệu cơ bản như đối tượng và sử dụng môi trường thời gian chạy để xác định động địa chỉ phương thức. Mặc dù mã giới hạn muộn cho phép một số kỹ thuật lập trình phức tạp như tính đa hình, nhưng nó đi kèm với một số chi phí liên quan mà chúng ta sẽ thấy ngay sau đây.

Nhưng trước tiên, hãy kiểm tra xem quá trình liên kết trễ được thực hiện như thế nào bằng cách sử dụng phản xạ trong C # (Phản chiếu là một cách mà mã sử dụng tại thời điểm chạy để xác định thông tin về giao diện của các lớp máy chủ; xem Chương 5)

Khi bạn liên kết muộn với đối tượng COM trong chương trình C #, bạn không cần tạo RCW cho thành phần COM. Thay vào đó, phương thức lớp GetTypeFromProgID của lớp Type được gọi để khởi tạo một đối tượng đại diện cho kiểu của đối tượng COM. Lớp Type là một thành viên của không gian tên System.Runtime.InteropServices và trong đoạn mã bên dưới, chúng tôi định cấu hình đối tượng Type cho cùng một thành phần COM truy cập dữ liệu được sử dụng trong các ví dụ trước:


Gõ objCustomerTableType;

Khi có một đối tượng Kiểu đóng gói thông tin kiểu của đối tượng COM, nó được sử dụng để tạo một thể hiện của chính đối tượng COM. Điều này được thực hiện bằng cách chuyển một đối tượng Type tới phương thức lớp CreateInstance của Activator.CreateInstance khởi tạo một đối tượng COM và trả về một tham chiếu giới hạn cuối cho nó, có thể được lưu trữ trong một tham chiếu của đối tượng kiểu.

đối tượng objCustomerTable;
objCustomerTable = Activator.CreateInstance (objCustomerTableType);

Thật không may, không thể gọi các phương thức trực tiếp trên một tham chiếu của đối tượng kiểu. Để truy cập đối tượng COM, bạn phải sử dụng phương thức InvokeMember của đối tượng Loại đã được tạo trước. Khi phương thức InvokeMember được gọi, nó được truyền một tham chiếu đến một đối tượng COM, cùng với tên của phương thức COM sẽ được gọi và một mảng kiểu đối tượng của tất cả các đối số đến của phương thức.

ObjCustomerTableType.InvokeMember ("Xóa", BindingFlags.InvokeMethod, null, objCustomerTable, aryInputArgs);

Nhắc lại một lần nữa chuỗi các hành động:

1. Tạo đối tượng Type cho kiểu đối tượng COM bằng phương thức lớp Type.GetTypeFromProgID ().

2. Sử dụng đối tượng Loại này để tạo đối tượng COM bằng Activator.CreateInstance ().

3. Các phương thức được gọi trên đối tượng COM bằng cách gọi phương thức InvokeMember trên đối tượng Type và chuyển tham chiếu đối tượng tới nó làm đối số đầu vào. Dưới đây là mã mẫu kết hợp tất cả những điều này thành một khối:

sử dụng System.Runtime.InteropServices;
Gõ objCustomerTableType;
đối tượng objCustomerTable;
objCustomerTableType = Type.GetTypeFromProgID ("DataAccess.CustomerTable");
objCustomerTable = Activator.CreateInstance (objCustomerTableType);
objCustomerTableType.InvokeMember ("Xóa", BindingFlags, InvokeMethod, null, objCustomerTable, aryInputArgs);
objCustomerTableType = Type.GetTypeFromProgID ("DataAccess.CustomerTable");

Mặc dù các tính năng ràng buộc muộn của C # tránh được những khó khăn của RCW, nhưng có một số hạn chế mà bạn cần lưu ý.

Thứ nhất, ràng buộc muộn có thể nguy hiểm. Khi sử dụng liên kết sớm, trình biên dịch có thể truy vấn thư viện kiểu của thành phần COM để xác minh rằng tất cả các phương thức được gọi trên đối tượng COM thực sự tồn tại. Với liên kết muộn, không có gì để ngăn chặn lỗi đánh máy trong lệnh gọi đến phương thức InvokeMember (), có thể tạo ra lỗi thời gian chạy.

"Liên kết muộn" là một trong những thuật ngữ khoa học máy tính giống như "gõ mạnh", có nghĩa là những điều khác nhau đối với những người khác nhau. Tôi nghĩ rằng tôi có thể mô tả ý nghĩa của tôi về nó.

Trước hết, “ràng buộc” là gì? Chúng ta sẽ không thể hiểu ràng buộc muộn có nghĩa là gì nếu chúng ta không biết thuật ngữ "ràng buộc" thực sự có nghĩa là gì.

Theo định nghĩa, trình biên dịch là một thiết bị lấy văn bản được viết bằng một ngôn ngữ và tạo ra mã bằng một ngôn ngữ khác "có nghĩa tương tự." Ví dụ: tôi đang phát triển một trình biên dịch lấy C # làm đầu vào và xuất ra CIL (*). Tất cả các tác vụ quan trọng được thực hiện bởi trình biên dịch có thể được chia thành ba nhóm lớn:

  • Phân tích cú pháp văn bản đầu vào
  • Phân tích cú pháp ngữ nghĩa
  • Tạo văn bản đầu ra - trong bài viết này, chúng tôi không quan tâm đến giai đoạn này

Phân tích cú pháp văn bản đầu vào không biết gì về Ý nghĩa văn bản đã phân tích; phân tích cú pháp chủ yếu quan tâm đến từ vựng cấu trúc chương trình (nghĩa là về ranh giới nhận xét, số nhận dạng, toán tử, v.v.), và sau đó, từ cấu trúc từ vựng này, nó được xác định ngữ pháp cấu trúc chương trình: ranh giới lớp, phương thức, toán tử, biểu thức, v.v.

Sau đó, trình phân tích ngữ nghĩa lấy kết quả của trình phân tích cú pháp và liên kết ý nghĩa của các phần tử cú pháp khác nhau. Ví dụ, khi bạn viết:

classX ()
lớp B ()
hạng D: B
{
public static void X () ()
public static void Y () (X ();)
}

sau đó trình phân tích cú pháp xác định sự hiện diện của ba lớp, một trong số chúng chứa hai phương thức, phương thức thứ hai chứa một toán tử, là một biểu thức gọi phương thức. Bộ phân tích ngữ nghĩa xác định rằng X trong biểu thức X (); đề cập đến phương thức D.X () hơn là kiểu X được khai báo ở trên. Đây là một ví dụ về "ràng buộc" theo nghĩa rộng nhất của từ này: ràng buộc là sự liên kết của một phần tử cú pháp chứa tên phương thức với phần logic của chương trình.

Khi nói đến "sớm" hoặc "muộn" "ràng buộc", luôn là việc xác định tên để gọi một phương thức. Tuy nhiên, theo quan điểm của tôi, định nghĩa này quá khắt khe. Tôi sẽ sử dụng thuật ngữ "ràng buộc" để mô tả quá trình mà trình phân tích ngữ nghĩa của trình biên dịch xác định rằng lớp D kế thừa lớp B và tên "B" được liên kết với tên lớp.

Hơn nữa, tôi cũng sẽ sử dụng thuật ngữ "ràng buộc" để mô tả các loại phân tích khác. Nếu bạn có biểu thức 1 * 2 + 1.0 trong chương trình của mình, thì tôi có thể nói rằng toán tử "+" được liên kết với một toán tử cài sẵn lấy hai số dấu phẩy động, cộng chúng và trả về số thứ ba. Thông thường mọi người không nghĩ đến việc liên kết tên "+" với một phương thức nào đó, nhưng tôi vẫn coi nó là "ràng buộc".

Nói đúng hơn, tôi có thể sử dụng thuật ngữ "ràng buộc" để tìm liên kết của các kiểu với các biểu thức không sử dụng trực tiếp tên của kiểu đó. Nói một cách không chính thức, trong ví dụ trên, biểu thức 1 * 2 được "liên kết" với kiểu int, mặc dù, rõ ràng, tên của kiểu này không được chỉ ra trong đó. Biểu thức cú pháp được liên kết chặt chẽ với thành phần ngữ nghĩa này, mặc dù nó không sử dụng trực tiếp tên tương ứng.

Vì vậy, nói chung, tôi sẽ nói rằng "ràng buộc" là bất kỳ liên kết nào của một số đoạn của cây cú pháp với một số phần tử logic của chương trình. (**)

Vậy thì sự khác biệt giữa ràng buộc "sớm" và "muộn" là gì? Mọi người thường nói về những khái niệm này như thể chúng là những lựa chọn loại trừ lẫn nhau: ràng buộc sớm hoặc ràng buộc sau. Như chúng ta sẽ sớm thấy, đây không phải là trường hợp; một số ràng buộc là hoàn toàn sớm, một số có một phần sớm và một phần muộn, và một số, thực sự, hoàn toàn muộn. Nhưng trước khi đạt được điều đó, chúng ta hãy xem xét liên quan đến cái gì ràng buộc xảy ra sớm hay muộn?

Thông thường, khi chúng ta nói về "liên kết sớm", chúng tôi muốn nói đến "ràng buộc được thực hiện bởi trình biên dịch và kết quả của ràng buộc được" nối cứng "vào mã được tạo"; nếu liên kết không thành công, chương trình không chạy vì trình biên dịch không thể tiến hành giai đoạn tạo mã. Bởi "liên kết muộn", chúng tôi muốn nói rằng "một số ràng buộc sẽ được thực hiện tại thời điểm chạy" và do đó các lỗi ràng buộc sẽ chỉ hiển thị vào thời gian chạy. Ràng buộc sớm và muộn đôi khi được gọi là "liên kết tĩnh" và "ràng buộc động"; liên kết tĩnh được thực hiện dựa trên thông tin "tĩnh" mà trình biên dịch đã biết, trong khi liên kết động được thực hiện dựa trên thông tin "động" chỉ được biết trong thời gian chạy.

Loại ràng buộc nào tốt hơn? Rõ ràng, không có lựa chọn nào trong số các lựa chọn rõ ràng là tốt hơn lựa chọn còn lại; nếu một trong những lựa chọn luôn vượt trội hơn lựa chọn kia, thì chúng tôi sẽ không thảo luận bất cứ điều gì với bạn bây giờ. Ưu điểm của việc ràng buộc sớm là chúng ta có thể chắc chắn rằng không có lỗi thời gian chạy; nhược điểm là thiếu tính linh hoạt ràng buộc muộn. Ràng buộc sớm giả định rằng tất cả thông tin cần thiết để đưa ra quyết định đúng sẽ được biết trước khi chương trình được thực thi; nhưng đôi khi thông tin này không có sẵn cho đến thời điểm thực hiện.

Tôi đã nói rằng ràng buộc tạo thành một phổ từ sớm đến muộn. Hãy xem một số ví dụ C # sẽ cho thấy cách chúng ta có thể chuyển từ ràng buộc sớm sang ràng buộc muộn.

Chúng tôi bắt đầu với một ví dụ về việc gọi một phương thức tĩnh X. Phân tích này rõ ràng là sớm. Chắc chắn rằng khi phương thức Y được gọi, phương thức D.X sẽ được gọi. Không có phần nào của phân tích này được hoãn lại trong thời gian chạy, vì vậy cuộc gọi này chắc chắn sẽ thành công.

Bây giờ, hãy xem ví dụ sau:

hạng B
{
public void M (double x) ()
public void M (intx) ()
}
lớp C
{
public static void X (B b, int d) (b.M (d);)
}

Bây giờ chúng ta có ít thông tin hơn. Chúng tôi thực hiện rất nhiều ràng buộc ban đầu; chúng ta biết rằng biến b thuộc kiểu B và phương thức B.M (int) được gọi. Tuy nhiên, không giống như ví dụ trước, chúng tôi không có trình biên dịch nào đảm bảo rằng cuộc gọi sẽ thành công, vì biến b có thể là null. Về bản chất, chúng tôi đang trì hoãn thời gian chạy để phân tích xem liệu người nhận cuộc gọi có hợp lệ hay không. Nhiều người không coi quyết định này là "ràng buộc" vì chúng tôi không liên kết cú pháp với phần tử chương trình. Hãy thực hiện cuộc gọi bên trong phương thức C gần đây hơn một chút bằng cách thay đổi lớp B:

hạng B
{
public virtual void M (double x) ()
public virtual void M (intx) ()
}

Bây giờ chúng tôi thực hiện một số phân tích tại thời điểm biên dịch; chúng ta biết rằng phương thức ảo B.M (int) sẽ được gọi. Chúng ta biết rằng cuộc gọi phương thức sẽ thành công, theo nghĩa là một phương thức như vậy tồn tại. Nhưng chúng tôi không biết phương thức nào sẽ được gọi trong thời gian chạy! Nó có thể là một phương thức được ghi đè trong một phương thức con cháu; mã hoàn toàn khác được định nghĩa trong một phần khác của chương trình có thể được gọi. Phái các phương pháp ảo là một dạng ràng buộc muộn; quyết định về phương thức nào được liên kết với cấu trúc cú pháp b.M (d) một phần được thực hiện bởi trình biên dịch và một phần vào thời gian chạy.

Làm thế nào về một ví dụ như vậy?

lớp C
{
public static void X (B b, dynamic d) (b.M (d);)
}

Bây giờ ràng buộc gần như hoàn toàn được hoãn lại trong thời gian chạy. Trong trường hợp này, trình biên dịch tạo mã thông báo cho Runtim ngôn ngữ động rằng phân tích tĩnh đã xác định rằng kiểu tĩnh của biến b là lớp B và phương thức đang được gọi là M, nhưng độ phân giải quá tải thực tế để xác định phương thức là BM ( int) hoặc BM (double) (hoặc không nếu d thuộc loại chuỗi, chẳng hạn) sẽ được thực thi trong thời gian chạy dựa trên thông tin này. (***)

lớp C
{
public static void X (dynamic b, dynamic d) (b.M (d);)
}

Bây giờ, tại thời điểm biên dịch, nó chỉ xác định rằng một phương thức có tên M được gọi trên một số kiểu. Đây thực tế là ràng buộc mới nhất, nhưng trên thực tế, chúng ta có thể đi xa hơn nữa:

lớp C
{
public static void X (đối tượng b, đối tượng d, chuỗi m, BindingFlags f)
{
b.GetType (). getMethod (m, f) .Invoke (b, d);
}
}

Bây giờ tất cả phân tích cú pháp được thực hiện trong quá trình ràng buộc muộn; chúng tôi thậm chí không biết tên là gì chúng ta sẽ liên kết với phương thức được gọi. Tất cả những gì chúng ta có thể biết là tác giả của X mong đợi rằng đối tượng được truyền vào b có một phương thức có tên chỉ định m, tương ứng với các cờ được truyền cho f, nhận các đối số được truyền cho d. Trong trường hợp này, chúng tôi không thể làm bất cứ điều gì tại thời điểm biên dịch. (****)

(*) Tất nhiên, kết quả được mã hóa ở định dạng nhị phân, không phải định dạng CIL mà con người có thể đọc được.

(**) Bạn có thể hỏi: "ràng buộc" và "phân tích ngữ nghĩa" có đồng nghĩa không; tất nhiên, phân tích ngữ nghĩa không gì khác hơn là sự liên kết của các yếu tố cú pháp với ý nghĩa của chúng! Ràng buộc là một phần quan trọng trong giai đoạn phân tích ngữ nghĩa của trình biên dịch, nhưng có nhiều dạng phân tích khác cần được thực hiện sau khi các phần thân của phương thức được "ràng buộc" hoàn toàn. Ví dụ, việc phân tích sự phân công xác định (xác định) không thể được gọi là "ràng buộc"; nó không phải là sự liên kết của các phần tử cú pháp với các phần tử chương trình cụ thể. Đúng hơn, phân tích này liên kết từ vựng địa điểm với các dữ kiện về các phần tử lập trình chẳng hạn như "biến cục bộ blah không được gán rõ ràng ở đầu khối này". Tương tự như vậy, tối ưu hóa các biểu thức số học là một hình thức phân tích ngữ nghĩa và không đề cập rõ ràng đến "ràng buộc".

(***) Trình biên dịch vẫn có thể thực hiện nhiều phân tích tĩnh. Giả sử lớp B là một lớp niêm phong không có phương thức nào có tên M. Ngay cả với các đối số động, chúng tôi đã biết tại thời điểm biên dịch rằng liên kết với phương thức M sẽ không thành công và chúng tôi có thể cho bạn biết về điều đó tại thời điểm biên dịch. Và trình biên dịch thực sự thực hiện loại phân tích đó; nhưng chính xác như thế nào - đây là một chủ đề tốt cho một cuộc trò chuyện khác.

(****) Theo một nghĩa nào đó, ví dụ này là một ví dụ điển hình ngược lại cho định nghĩa của tôi về ràng buộc; chúng tôi thậm chí không kết nối các yếu tố cú pháp với phương pháp; chúng tôi liên kết nội dung của chuỗi với phương thức.

Ràng buộc trong C ++

Hai mục tiêu chính trong sự phát triển của ngôn ngữ lập trình C ++ là hiệu quả bộ nhớ và tốc độ thực thi. Nó được coi là một cải tiến cho ngôn ngữ C, đặc biệt là cho các ứng dụng hướng đối tượng. Nguyên tắc cơ bản của C ++ là không có thuộc tính nào của ngôn ngữ dẫn đến chi phí bổ sung (cả bộ nhớ và tốc độ) nếu thuộc tính này không được lập trình viên sử dụng. Ví dụ: nếu tất cả hướng đối tượng của C ++ bị bỏ qua, thì phần còn lại sẽ chạy nhanh như C. Do đó, không có gì ngạc nhiên khi hầu hết các phương thức trong C ++ đều bị ràng buộc tĩnh (tại thời điểm biên dịch) chứ không phải động (tại thời gian chạy).

Ràng buộc phương thức trong ngôn ngữ này khá phức tạp. Đối với các biến thông thường (không phải con trỏ hoặc tham chiếu), nó được thực hiện ở chế độ tĩnh. Nhưng khi các đối tượng được biểu thị bằng con trỏ hoặc tham chiếu, liên kết động được sử dụng. Trong trường hợp thứ hai, việc lựa chọn một phương thức thuộc kiểu tĩnh hoặc động được quyết định bởi phương thức tương ứng có được khai báo với từ khóa ảo hay không. Nếu nó được khai báo theo cách đó, thì phương thức tra cứu thông báo dựa trên lớp động, nếu không, trên lớp tĩnh. Ngay cả trong trường hợp sử dụng ràng buộc động, tính hợp lệ của bất kỳ yêu cầu nào cũng được trình biên dịch xác định dựa trên lớp tĩnh của bộ thu.

Ví dụ: hãy xem xét khai báo sau của các lớp và biến toàn cục: class Mammal

printf ("không nói được");

hạng Chó: Động vật có vú công cộng

printf ("wouf wouf");

printf ("wouf wouf, well");

Mammal * fido = new Dog;

Biểu thức fred.speak () in ra "cant speak", nhưng việc gọi fido-> speak () cũng sẽ in ra "cant speak" vì phương thức tương ứng trong lớp Mammal không được khai báo là ảo. Biểu thức fido-> shell () không được trình biên dịch cho phép, ngay cả khi kiểu động cho fido là lớp Dog. Tuy nhiên, kiểu biến tĩnh chỉ là lớp Mammal.

Nếu chúng ta thêm từ ảo:

ảo void speak ()

printf ("không nói được");

thì chúng ta nhận được kết quả mong đợi cho biểu thức fido-> speak ().

Một thay đổi tương đối gần đây trong ngôn ngữ C ++ là việc bổ sung một phương tiện để nhận ra lớp động của một đối tượng. Chúng tạo thành hệ thống RTTI (Nhận dạng loại thời gian chạy).

Trong hệ thống RTTI, mỗi lớp có một cấu trúc typeinfo liên quan mã hóa các thông tin khác nhau về lớp. Trường dữ liệu tên Một trong các trường dữ liệu của cấu trúc này chứa tên của lớp dưới dạng một chuỗi văn bản. Hàm typeid có thể được sử dụng để phân tích cú pháp thông tin về kiểu dữ liệu. Do đó, lệnh sau sẽ in kiểu dữ liệu động chuỗi "Dog" cho fido. Trong ví dụ này, cần phải tham chiếu đến biến con trỏ fido để đối số là giá trị mà con trỏ tham chiếu đến, chứ không phải chính con trỏ:

cout<< «fido is a» << typeid(*fido).name() << endl;

Bạn cũng có thể hỏi, bằng cách sử dụng hàm before member, liệu một cấu trúc với thông tin kiểu dữ liệu có tương ứng với một lớp con của lớp tương ứng với một cấu trúc khác hay không. Ví dụ, hai câu lệnh sau đây cho kết quả true và false:

if (typeid (* fido) .before (typeid (fred)))…

if (typeid (fred) .before (typeid (lassie)))…

Trước khi hệ thống RTTI ra đời, một thủ thuật lập trình tiêu chuẩn là viết mã các phương thức một cách rõ ràng là các thể hiện trong hệ thống phân cấp lớp. Ví dụ: để kiểm tra xem giá trị của các biến kiểu Động vật là kiểu Mèo hay kiểu Chó, người ta có thể xác định hệ thống các phương pháp sau:

int ảo isaDog ()

int ảo isaCat ()

hạng Chó: Động vật có vú công cộng

int ảo isaDog ()

lớp Cat: Động vật có vú công cộng

int ảo isaCat ()

Bây giờ bạn có thể sử dụng fido-> isaDog () để xác định xem giá trị hiện tại của fido có phải là Chó hay không. Nếu một giá trị khác 0 được trả về, thì bạn có thể ép kiểu biến thành kiểu dữ liệu mong muốn.

Bằng cách trả về một con trỏ thay vì một số nguyên, chúng tôi kết hợp phân lớp và ép kiểu. Điều này tương tự với một phần khác của hệ thống RTTI được gọi là dynamic_cast, chúng tôi sẽ mô tả ngắn gọn. Nếu một hàm trong lớp Mammal trả về một con trỏ đến một Con chó, thì lớp Chó phải được khai báo trước. Kết quả của phép gán là một con trỏ null hoặc một tham chiếu hợp lệ đến lớp Dog. Vì vậy, việc kiểm tra kết quả vẫn cần được thực hiện, nhưng chúng tôi loại bỏ sự cần thiết của một bản đánh máy. Điều này được thể hiện trong ví dụ sau:

lớp Dog; // mô tả sơ bộ

Virtual Dog * isaDog ()

Virtual Cat * isaCat ()

hạng Chó: Động vật có vú công cộng

Virtual Dog * isaDog ()

lớp Cat: Động vật có vú công cộng

Virtual Cat * isaCat ()

toán tử lassie = fido-> isaDog (); bây giờ chúng tôi sẽ luôn luôn làm điều đó. Do đó, biến lassie chỉ được đặt thành giá trị khác 0 nếu fido có lớp Dog động. Nếu fido không thuộc sở hữu của Dog, thì lassie sẽ được gán một con trỏ null.

lassie = fido-> isaDog ();

… // fido thực sự là một con chó

... // chuyển nhượng không thành công

… // fido không thuộc loại chó

Mặc dù người lập trình có thể sử dụng phương pháp này để đảo ngược tính đa hình, nhưng nhược điểm của phương pháp này là nó yêu cầu thêm các phương thức vào cả lớp cha và lớp con. Nếu nhiều phần tử con xuất phát từ một lớp cha chung, thì phương thức này trở nên khó sử dụng. Nếu các thay đổi không được phép trong lớp cha, thì kỹ thuật này hoàn toàn không thể thực hiện được.

Vì những vấn đề như vậy là phổ biến, một giải pháp chung đã được tìm thấy cho chúng. Hàm mẫu dynamic_cast nhận một kiểu làm đối số mẫu và cũng giống như hàm được định nghĩa ở trên, trả về giá trị của đối số (nếu ép kiểu hợp pháp) hoặc null (nếu ép kiểu không được phép). Một nhiệm vụ tương đương với một nhiệm vụ được thực hiện trong ví dụ trước có thể được viết như thế này:

// chỉ chuyển đổi nếu fido là một con chó

lassie = dynamic_cast< Dog* >(fido);

// sau đó kiểm tra xem quá trình truyền thành công hay không

Ba kiểu truyền khác (static_cast, const_cast và reinterpret_cast) đã được thêm vào ngôn ngữ C ++, nhưng chúng được sử dụng trong những trường hợp đặc biệt và do đó không được mô tả ở đây. Các lập trình viên được khuyến khích sử dụng chúng như một phương tiện an toàn hơn thay vì cơ chế đúc kiểu cũ.

2. Phần thiết kế