Tại sao từ khóa 'out' được sử dụng trong hai bối cảnh dường như khác nhau?


11

Trong C #, outtừ khóa có thể được sử dụng theo hai cách khác nhau.

  1. Là một công cụ sửa đổi tham số trong đó một đối số được truyền bằng tham chiếu

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
  2. Là một sửa đổi tham số loại để xác định hiệp phương sai .

    // Covariant interface. 
    interface ICovariant<out R> { }
    
    // Extending covariant interface. 
    interface IExtCovariant<out R> : ICovariant<R> { }
    
    // Implementing covariant interface. 
    class Sample<R> : ICovariant<R> { }
    
    class Program
    {
        static void Test()
        {
            ICovariant<Object> iobj = new Sample<Object>();
            ICovariant<String> istr = new Sample<String>();
    
            // You can assign istr to iobj because 
            // the ICovariant interface is covariant.
            iobj = istr;
        }
    }

Câu hỏi của tôi là: tại sao?

Đối với người mới bắt đầu, kết nối giữa hai người dường như không trực quan . Việc sử dụng với thuốc generic dường như không liên quan gì đến việc chuyển qua tham chiếu.

Trước tiên tôi đã học được những gì outliên quan đến việc truyền các đối số bằng cách tham chiếu và điều này cản trở sự hiểu biết của tôi về việc sử dụng xác định hiệp phương sai với tổng quát.

Có một kết nối giữa những sử dụng mà tôi đang thiếu?


5
Kết nối dễ hiểu hơn một chút nếu bạn nhìn vào cách sử dụng hiệp phương sai và chống chỉ định trong System.Func<in T, out TResult>ủy nhiệm .
rwong

4
Ngoài ra, hầu hết các nhà thiết kế ngôn ngữ cố gắng giảm thiểu số lượng từ khóa và thêm từ khóa mới vào một số ngôn ngữ hiện có với một cơ sở mã lớn là điều khó khăn (có thể xung đột với một số mã hiện có sử dụng từ đó làm tên)
Basile Starynkevitch

Câu trả lời:


20

Có một kết nối, tuy nhiên nó là một chút lỏng lẻo. Trong C #, các từ khóa ỏin và và xuất hiện vì tên của chúng gợi ý cho đầu vào và đầu ra. Điều này rất rõ ràng trong trường hợp các tham số đầu ra, nhưng ít sạch hơn những gì nó phải làm với các tham số mẫu.

Hãy xem nguyên tắc thay thế Liskov :

...

Nguyên tắc của Liskov áp đặt một số yêu cầu tiêu chuẩn đối với chữ ký đã được áp dụng trong các ngôn ngữ lập trình hướng đối tượng mới hơn (thường ở cấp độ của các lớp thay vì các loại; xem phân loại danh nghĩa so với cấu trúc để phân biệt):

  • Chống chỉ định của các đối số phương thức trong kiểu con.
  • Hiệp phương sai của các kiểu trả về trong kiểu con.

...

Xem làm thế nào chống chỉ định được liên kết với đầu vào và hiệp phương sai được liên kết với đầu ra? Trong C # nếu bạn gắn cờ một biến mẫu với biến outnó thành biến số, nhưng xin lưu ý rằng bạn chỉ có thể làm điều này nếu tham số loại được đề cập chỉ xuất hiện dưới dạng đầu ra (kiểu trả về hàm). Vì vậy, sau đây là không hợp lệ:

interface I<out T>
{
  void func(T t); //Invalid variance: The type parameter 'T' must be
                  //contravariantly valid on 'I<T>.func(T)'.
                  //'T' is covariant.

}

Similary nếu bạn gắn cờ một tham số loại in, điều đó có nghĩa là bạn chỉ có thể sử dụng nó làm đầu vào (tham số hàm). Vì vậy, sau đây là không hợp lệ:

interface I<in T>
{
  T func(); //Invalid variance: The type parameter 'T' must
            //be covariantly valid on 'I<T>.func()'. 
            //'T' is contravariant.

}

Vì vậy, để tóm tắt, kết nối với outtừ khóa là với các tham số chức năng, điều đó có nghĩa là nó là một tham số đầu ra và đối với tham số loại, điều đó có nghĩa là loại chỉ được sử dụng trong ngữ cảnh đầu ra .

System.Funccũng là một ví dụ tốt những gì rwong đề cập trong bình luận của mình. Trong System.Functất cả các tham số đầu vào ar flaged with in, và tham số đầu ra được flaged với out. Lý do là chính xác những gì tôi mô tả.


2
Câu trả lời tốt đẹp! Tiết kiệm cho tôi một số chờ đợi cho nó gõ gõ! Nhân tiện: một phần của LSP mà bạn đã trích dẫn đã thực sự được biết đến từ lâu trước Liskov. Đó chỉ là quy tắc phân nhóm tiêu chuẩn cho các loại chức năng. (Các loại tham số là chống chỉ định, các loại trả về là covariant). Tính mới của cách tiếp cận của Liskov là a) diễn đạt các quy tắc không phải về mặt đồng phạm mà là về khả năng thay thế hành vi (như được định nghĩa bởi trước / hậu điều kiện) và b) quy tắc lịch sử , điều này có thể áp dụng tất cả lý do này đến các kiểu dữ liệu có thể thay đổi, điều mà trước đây không thể có được.
Jörg W Mittag

10

@ Gábor đã giải thích kết nối (chống chỉ định cho tất cả những gì đi vào "trong", hiệp phương sai cho tất cả những gì đi "ra"), nhưng tại sao lại sử dụng từ khóa?

Vâng, từ khóa rất đắt tiền. Bạn không thể sử dụng chúng làm định danh trong các chương trình của bạn. Nhưng chỉ có rất nhiều từ trong tiếng Anh. Vì vậy, đôi khi bạn gặp phải xung đột và bạn phải lúng túng đổi tên các biến, phương thức, trường, thuộc tính, lớp, giao diện hoặc cấu trúc của mình để tránh xung đột với từ khóa. Ví dụ, nếu bạn đang làm người mẫu ở trường, bạn gọi một lớp là gì? Bạn không thể gọi nó là một lớp, bởi vì classlà một từ khóa!

Thêm một từ khóa để một ngôn ngữ thậm chí còn nhiều tốn kém. Về cơ bản, nó làm cho tất cả các mã sử dụng từ khóa này làm định danh bất hợp pháp, phá vỡ tính tương thích ngược ở mọi nơi.

Các từ khóa inoutđã tồn tại, vì vậy chúng có thể được sử dụng lại.

Họ có thể đã thêm các từ khóa theo ngữ cảnh chỉ là từ khóa trong ngữ cảnh của danh sách tham số loại, nhưng họ sẽ chọn từ khóa nào? covariantcontravariant? +-(như Scala đã làm chẳng hạn)? superextendsgiống như Java đã làm gì? Bạn có thể nhớ ra khỏi đỉnh đầu của bạn những tham số nào là covariant và contravariant?

Với giải pháp hiện tại, có một kiểu ghi nhớ đẹp: tham số loại đầu ra lấy outtừ khóa, tham số loại đầu vào lấy intừ khóa. Lưu ý sự đối xứng đẹp với các tham số phương thức: tham số đầu ra lấy outtừ khóa, tham số đầu vào lấy intừ khóa (tốt, thực tế, không có từ khóa nào cả, vì đầu vào là mặc định, nhưng bạn hiểu ý).

[Lưu ý: nếu bạn nhìn vào lịch sử chỉnh sửa, bạn sẽ thấy rằng ban đầu tôi thực sự đã chuyển hai xung quanh trong câu giới thiệu của mình. Và tôi thậm chí đã có một upvote trong thời gian đó! Điều này chỉ để cho bạn thấy tầm quan trọng của việc ghi nhớ đó thực sự quan trọng như thế nào.]


Cách để nhớ đồng biến so với phương sai là xem xét điều gì xảy ra nếu một hàm trong giao diện lấy tham số của loại giao diện chung. Nếu ai có interface Accepter<in T> { void Accept(T it);};, một Accepter<Foo<T>>sẽ chấp nhận Tlàm tham số đầu vào nếu Foo<T>chấp nhận nó làm tham số đầu ra và ngược lại. Do đó, chống chỉ định. Ngược lại, interface ISupplier<out T> { T get();};một Supplier<Foo<T>>ý chí có bất kỳ loại phương sai nào Foocó - do đó đồng phương sai .
supercat
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.