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

时间:2010-05-20 17:33:39

标签: c# covariance contravariance variance

该陈述的含义是什么?

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)并且有效。

4 个答案:

答案 0 :(得分:43)

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

“in”表示粗略地说,“仅出现在输入位置”。

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

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

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两个含义;这会造成一些混乱。让我清楚一点:

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

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

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

遗憾的是,interface IFoo<in T, out U> { void Foo(in T t, out U u); }是非法的,因为它看起来应该有效。它无法工作,因为从CLR验证程序的角度来看,它们都是ref参数,因此是可读写的。

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

答案 1 :(得分:1)

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

public delegate R MyDelegate<out R, in A>(ref A arg);

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

public delegate void MyDelegate<R, in A>(A arg, out R s);

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

public delegate void MyDelegate<out R, in A>(A arg, out R s);

答案 2 :(得分:1)

可以在没有参数的情况下使用协方差,但是您需要两个结构。例如,您可以将out参数放在扩展方法上:

public static class ResultExtension{
    public static bool TryGetValue<T>(this IResult<T> self, out T res) {
        if (self.HasValue) {
            res = self.Value;
            return true;

        }
        res = default;
        return false;
    }
}

public interface IResult<out T>
{
    bool HasValue { get; }
    T Value { get; }
}

答案 3 :(得分:0)

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

interface IFoo<out T>
{
    T Get();

    //bool TryGet(out T value); // doesn't work: Invalid variance: The type parameter 'T' must be invariantly valid on 'IFoo<T>.TryGet(out T)'. 'T' is covariant.

    bool TryGet(Action<T> value); // works!
}