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

问题描述 投票:13回答:4

该陈述是什么意思?

From here

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

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

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

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();
    }

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

c# covariance contravariance variance
4个回答
45
投票

粗略地说,“出”意味着“仅出现在产出位置”。

粗略地说,“在”中意味着“仅出现在输入位置”。

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

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

delegate void Foo</*???*/ T>(ref T item);

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);

好吧我的猫,我们只是做了一个猫皮。 “出局”不合法。

怎么样“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);

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

out参数怎么样?

delegate void Foo</*???*/T>(out T item);

?现在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(); 
    }
}

我们又一次做了一只猫吠。我们不能让T“出局”。

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


更新:C#7添加了in作为形式参数声明,这意味着我们现在有inout两个含义;这会造成一些混乱。让我清楚一点:

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

相反,类型参数声明中的inout分别表示“此类型参数不得以协变方式使用”,并且“此类型参数不得以逆变方式使用”。

如上所述,我们为这些修饰符选择了refref,因为如果我们看到in,那么out用于“输入”位置,而in用于“输出”位置。虽然这不是严格正确的,但在99.9%的用例中它确实是一个有用的助记符。

不幸的是,out是非法的,因为它看起来应该有效。它无法工作,因为从CLR验证器的角度来看,这些都是in参数,因此是读写。

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


1
投票

这意味着您不能拥有以下声明:

out

编辑:@Eric Lippert纠正我,这个仍然是合法的:

IFoo<in T, out U>

它实际上是有道理的,因为R泛型参数未标记为变体,因此它不违反规则。但是,这个仍然是非法的:

T

1
投票

可以将协方差与out参数一起使用,但是您需要两个结构。例如,您可以将out参数放在扩展方法上:

U

0
投票

但是,可以编译以下代码:

interface IFoo<in T, out U> { void Foo(in T t, out U u); }
© www.soinside.com 2019 - 2024. All rights reserved.