«out» означает, грубо говоря, «появляется только в выходных позициях».
«in» означает, грубо говоря, «появляется только в позициях ввода».
Реальная история немного сложнее, но ключевые слова были выбраны, потому что в большинстве случаев это так.
Рассмотрим метод интерфейса или метод, представленный делегатом:
delegate void Foo</*???*/ T>(ref T item);
T появляется в позиции ввода? Да. Вызывающий может передать значение T через элемент; Колли Фу может прочитать это. Поэтому T нельзя пометить как «out».
T появляется в выходной позиции? Да. Вызываемый может записать новое значение в элемент, который затем может прочитать вызывающий. Поэтому T нельзя пометить как «in».
Поэтому, если T появляется в формальном параметре «ref», T не может быть помечен как входящий или выходной.
Давайте посмотрим на некоторые реальные примеры того, как все идет не так. Предположим, это было законно:
delegate void X<out T>(ref T item);
...
X<Dog> x1 = (ref Dog d)=>{ d.Bark(); }
X<Animal> x2 = x1; // covariant;
Animal a = new Cat();
x2(ref a);
Хорошо, собаку мои кошки, мы только что сделали лай кошки. «вне» не может быть законным.
А как насчет "в"?
delegate void X<in T>(ref T item);
...
X<Animal> x1 = (ref Animal a)=>{ a = new Cat(); }
X<Dog> x2 = x1; // contravariant;
Dog d = new Dog();
x2(ref d);
И мы просто помещаем кошку в переменную, которая может содержать только собак. T также не может быть помечен "в".
А как насчет параметра out?
delegate void Foo</*???*/T>(out T item);
? Теперь T появляется только в выходной позиции. Должно ли быть законным сделать T помеченным как "out"?
К сожалению нет. "out" на самом деле не отличается от "ref" за кадром. Единственное различие между «out» и «ref» состоит в том, что компилятор запрещает чтение из параметра out, прежде чем он будет назначен вызываемым объектом, и что компилятору требуется присвоение, прежде чем вызываемый объект возвратится нормально. Кто-то, кто написал реализацию этого интерфейса на языке .NET, отличном от C # , сможет прочитать элемент до его инициализации, и, следовательно, его можно будет использовать в качестве входных данных. Поэтому мы запрещаем помечать T как «out» в этом случае. Это прискорбно, но мы ничего не можем с этим поделать; мы должны соблюдать правила безопасности типов CLR.
Кроме того, правило "out" параметров заключается в том, что они не могут использоваться для ввода до того, как они будут записаны в . Нет правила, что их нельзя использовать для ввода после , в который они записаны. Предположим, мы разрешили
delegate void X<out T>(out T item);
class C
{
Animal a;
void M()
{
X<Dog> x1 = (out Dog d) =>
{
d = null;
N();
if (d != null)
d.Bark();
};
x<Animal> x2 = x1; // Suppose this were legal covariance.
x2(out this.a);
}
void N()
{
if (this.a == null)
this.a = new Cat();
}
}
Еще раз мы сделали лай кошки. Мы не можем позволить T быть "вне".
Очень глупо использовать параметры для ввода таким способом, но допустимо.
ОБНОВЛЕНИЕ: C # 7 добавил in
в качестве формального объявления параметра, что означает, что теперь у нас есть и in
и out
, означающие две вещи; это создаст некоторую путаницу. Позвольте мне прояснить это:
in
, out
и ref
при объявлении формального параметра в списке параметров означает, что «этот параметр является псевдонимом переменной, предоставленной вызывающей стороной».
ref
означает «вызываемый может прочитать или записать переменную с псевдонимом, и она должна быть известна для назначения перед вызовом.
out
означает «вызываемый должен записать переменную с псевдонимом через псевдоним, прежде чем он вернется нормально». Это также означает, что вызываемый объект не должен читать переменную с псевдонимом через псевдоним перед тем, как записать ее, поскольку переменная не может быть точно назначена.
in
означает «вызываемый может прочитать псевдоним, но не записывает в него псевдоним». Цель in
- решить редкую проблему производительности, при которой большая структура должна передаваться «по значению», но это дорого. В качестве детали реализации, параметры in
обычно передаются через значение размера указателя, которое быстрее, чем копирование по значению, но медленнее при разыменовании.
- С точки зрения CLR
in
, out
и ref
- это одно и то же; правила о том, кто какие переменные читает и пишет, в какие моменты времени CLR не знает или не заботится.
- Поскольку именно CLR обеспечивает соблюдение правил о дисперсии, правила, применяемые к
ref
, также применяются к параметрам in
и out
.
Напротив, in
и out
в объявлениях параметров типа означают «этот параметр типа не должен использоваться ковариантным образом» и «этот параметр типа не должен использоваться контравариантным образом», соответственно.
Как отмечалось выше, мы выбрали in
и out
для этих модификаторов, потому что если мы увидим IFoo<in T, out U>
, тогда T
используется в позициях «ввода», а U
используется в позициях «вывода». Хотя это не строго истина, в 99,9% случаев достаточно верно, что это полезная мнемоника.
К сожалению, interface IFoo<in T, out U> { void Foo(in T t, out U u); }
незаконно, потому что похоже, что оно должно работать. Это не может работать, потому что с точки зрения верификатора CLR оба являются параметрами ref
и, следовательно, доступны для чтения и записи.
Это всего лишь одна из тех странных, непреднамеренных ситуаций, когда две функции, которые по логике должны работать вместе, не работают вместе из-за подробностей реализации.