在C#中引用和输出参数,不能标记为变体

Wat*_* v2 13 c# covariance contravariance variance

该陈述是什么意思?

从这里

在C#中引用和输出参数,不能标记为变体.

1)是否意味着不能做以下事情.

public class SomeClass<R, A>: IVariant<R, A>
{
    public virtual R DoSomething( ref A args )
    {
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

2)或者它是否意味着我不能拥有以下内容.

public delegate R Reader<out R, in A>(A arg, string s);

public static void AssignReadFromPeonMethodToDelegate(ref Reader<object, Peon> pReader)
{
    pReader = ReadFromPeon;
}

static object ReadFromPeon(Peon p, string propertyName)
    {
        return p.GetType().GetField(propertyName).GetValue(p);
    }

static Reader<object, Peon> pReader;

static void Main(string[] args)
    {
        AssignReadFromPeonMethodToDelegate(ref pReader);
        bCanReadWrite = (bool)pReader(peon, "CanReadWrite");

        Console.WriteLine("Press any key to quit...");
        Console.ReadKey();
    }
Run Code Online (Sandbox Code Playgroud)

我试过(2)并且它有效.

Eri*_*ert 46

粗略地说,"出"意味着"仅出现在产出位置".

粗略地说,"在"中意味着"仅出现在输入位置".

真实的故事比这更复杂,但选择关键词是因为大部分时间都是如此.

考虑一个接口的方法或委托代表的方法:

delegate void Foo</*???*/ T>(ref T item);
Run Code Online (Sandbox Code Playgroud)

T出现在输入位置吗?是.调用者可以在via项中传递T值; 被叫者Foo可以读取它.因此T不能标记为"out".

T出现在输出位置吗?是.被调用者可以向项目写入新值,然后调用者可以读取该值.因此T不能标记为"in".

因此,如果T出现在"ref"形式参数中,则T不能标记为in或out.

让我们看看出现问题的一些真实例子.假设这是合法的:

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);
Run Code Online (Sandbox Code Playgroud)

好吧我的猫,我们只是做了一个猫皮."出局"不合法.

怎么样"in"?

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);
Run Code Online (Sandbox Code Playgroud)

我们只是把一只猫放在一个只能容纳狗的变量中.T也不能标记为"in".

out参数怎么样?

delegate void Foo</*???*/T>(out T item);
Run Code Online (Sandbox Code Playgroud)

?现在T只出现在输出位置.将T标记为"out"是否合法?

很不幸的是,不行."out"实际上与幕后的"ref"没有什么不同."out"和"ref"之间的唯一区别是编译器禁止在被调用者分配之前从out参数读取,并且编译器在被调用者正常返回之前需要赋值.以C#以外的.NET语言编写此接口实现的人可以在初始化之前从该项读取,因此可以将其用作输入.因此,在这种情况下,我们禁止将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(); 
    }
}
Run Code Online (Sandbox Code Playgroud)

我们又一次做了一只猫吠.我们不能让T"出局".

以这种方式输出参数用于输入是非常愚蠢的,但是合法的.


更新:C#7添加in了一个正式的参数声明,这意味着我们现在有两个in并且out意味着两件事; 这会造成一些混乱.让我清楚一点:

  • in,out以及参数列表中ref形式参数声明意味着"此参数是调用者提供的变量的别名".
  • ref 表示"被调用者可以读取或写入别名变量,并且必须知道在调用之前分配它.
  • out表示"被调用者必须在正常返回之前通过别名编写别名变量".它还意味着被调用者在写入之前不能通过别名读取别名变量,因为该变量可能没有明确赋值.
  • in表示"被调用者可以读取别名变量但不通过别名写入".其目的in是解决一个罕见的性能问题,即大型结构必须"按值"传递,但这样做很昂贵.作为实现细节,in参数通常通过指针大小的值传递,这比按值复制更快,但在解除引用时更慢.
  • 从CLR的角度来看,in,outref都是同样的事情; 关于谁在何时读取和写入变量的规则,CLR不知道或不关心.
  • 由于CLR强制执行有关方差的规则,因此适用的规则ref也适用于inout参数.

与此相反,inout上型参数声明意味着"这种类型的参数必须不能在协变的方式使用"及"这种类型的参数必须不能在逆变方式使用".

如上所述,我们选择inout使用那些修饰符,因为如果我们看到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参数,因此是读写.

这只是一种奇怪的,无意的情况,其中逻辑上应该一起工作的两个功能在实现细节原因上不能很好地协同工作.

  • 嗨,埃里克,在我向你保证你的讨论得到了忠实的追随之前,我将不得不睡个好觉.现在,感谢一百万.你对工作的热情显然是无法克服的,无法超越(如果有这样的话).虽然我很欣赏将ref/out类型作为变量参数或返回类型的不可能性背后的原因,但是当我很好地休息以确定你所付出的努力时,我会重新阅读你的答案.再次感谢你多次. (3认同)
  • supercat描述的refness称为"copy-in-copy-out",有些语言使用它.这种通过引用传递变量的想法,或等价地,为变量创建别名,在我看来是一个高效的实现细节被表现为混淆语言特征的情况.很多人不理解它是如何工作的事实表明它可能不是一个很好的功能. (2认同)