Почему ключевое слово «out» используется в двух, казалось бы, разрозненных контекстах?

11

В C # outключевое слово может использоваться двумя различными способами.

  1. В качестве модификатора параметра, в котором аргумент передается по ссылке

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
  2. В качестве модификатора параметра типа указать ковариацию .

    // 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;
        }
    }

Мой вопрос: почему?

Для новичка связь между ними не кажется интуитивной . Использование с генериками, похоже, не имеет ничего общего с передачей по ссылке.

Сначала я узнал, что outбыло связано с передачей аргументов по ссылке, и это помешало моему пониманию использования определения ковариации с генериками.

Есть ли связь между этими видами использования, которые я пропускаю?

Роуэн Фриман
источник
5
Связь немного более понятна, если вы посмотрите на использование ковариации и контравариантности в System.Func<in T, out TResult>делегате .
rwong
4
Кроме того, большинство разработчиков языков стараются свести к минимуму количество ключевых слов, и добавление нового ключевого слова в некоторый существующий язык с большой кодовой базой является болезненным (возможный конфликт с некоторым существующим кодом, использующим это слово в качестве имени)
Василий Старынкевич,

Ответы:

20

Есть связь, но она немного свободна. В C # ключевые слова «in» и «out» в качестве названия предлагают обозначать ввод и вывод. Это очень ясно в случае выходных параметров, но менее понятно, что это нужно делать с параметрами шаблона.

Давайте посмотрим на принцип замещения Лискова :

...

Принцип Лискова предъявляет некоторые стандартные требования к сигнатурам, которые были приняты в более новых объектно-ориентированных языках программирования (обычно на уровне классов, а не типов; см. Различие между номинальным и структурным подтипами):

  • Контравариантность аргументов метода в подтипе.
  • Ковариантность возвращаемых типов в подтипе.

...

Посмотрите, как контравариантность связана с вводом, а ковариация связана с выходом? В C # если вы пометите переменную шаблона с помощью, outчтобы сделать ее ковариантной, но, пожалуйста, обратите внимание, что вы можете делать это только в том случае, если упомянутый параметр типа отображается только как выходной (тип возврата функции). Таким образом, следующее недействительно:

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.

}

Аналогично, если вы помечаете параметр типа с помощью in, это означает, что вы можете использовать его только как ввод (параметр функции). Таким образом, следующее недействительно:

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

}

Подводя итог, можно outсказать , что связь с ключевым словом заключается в том, что с параметрами функции это означает, что это выходной параметр, а для параметров типа это означает, что тип используется только в выходном контексте.

System.FuncЭто также хороший пример того, что Руонг упомянул в своем комментарии. Во System.Funcвсех входных параметров ар flaged с in, а выходной параметр flaged с out. Причина именно то, что я описал.

Габор Ангьял
источник
2
Хороший ответ! Спасло меня немного ... подожди ... набрав! Кстати, та часть LSP, которую вы цитировали, была на самом деле известна задолго до Лискова. Это просто стандартные правила подтипов для типов функций. (Типы параметров контравариантны, типы возвращаемых значений - ковариантны). Новизна подхода Лискова заключалась в том, чтобы: а) формулировать правила не с точки зрения со-/ контравариантности, а с точки зрения замещаемости поведения (как определено до- / постусловиями) и б) исторического правила , которое позволяет применять все эти рассуждения к изменяемым типам данных, что ранее было невозможно.
Йорг Миттаг
10

@ Габор уже объяснил связь (контравариантность для всего, что идет «внутрь», ковариантность для всего, что идет «снаружи»), но зачем вообще повторно использовать ключевые слова?

Ну, ключевые слова очень дороги. Вы не можете использовать их в качестве идентификаторов в своих программах. Но в английском языке очень много слов. Поэтому иногда вы сталкиваетесь с конфликтами, и вам приходится неловко переименовывать свои переменные, методы, поля, свойства, классы, интерфейсы или структуры, чтобы избежать столкновения с ключевым словом. Например, если вы моделируете школу, что вы называете классом? Вы не можете назвать это классом, потому что classэто ключевое слово!

Добавление ключевого слова на язык еще более дорогим. Это в основном делает весь код, который использует это ключевое слово как идентификатор, недопустимым, нарушая обратную совместимость повсюду.

inИ outключевые слова уже существует, так что они могли просто быть использованы повторно.

Они могли бы добавить контекстные ключевые слова, которые являются только ключевыми словами в контексте списка параметров типа, но какие ключевые слова они бы выбрали? covariantа contravariant? +и -(как, например, Scala)? superи extendsкак Java сделал? Не могли бы вы вспомнить, какие параметры являются ковариантными и контравариантными?

В текущем решении есть хорошая мнемоника: параметры выходного типа получают outключевое слово, параметры входного типа получают inключевое слово. Обратите внимание на хорошую симметрию с параметрами метода: выходные параметры получают outключевое слово, входные параметры получают inключевое слово (ну, вообще-то, никакого ключевого слова вообще нет, так как ввод по умолчанию, но вы понимаете).

[Примечание: если вы посмотрите на историю изменений, вы увидите, что я фактически поменял их в своем вступительном предложении. И я даже получил голос за это время! Это просто показывает, насколько важен этот мнемоник.]

Йорг Миттаг
источник
Способ запоминания по сравнению с противоположной дисперсией заключается в рассмотрении того, что происходит, если функция в интерфейсе принимает параметр общего типа интерфейса. Если есть interface Accepter<in T> { void Accept(T it);};, a Accepter<Foo<T>>будет принимать Tв качестве входного параметра, если Foo<T>принимает его в качестве выходного параметра, и наоборот. Таким образом, против -variance. В противоположность этому , будет иметь какой бы то ни дисперсионный есть - таким образом , совместное -variance. interface ISupplier<out T> { T get();};Supplier<Foo<T>>Foo
суперкат