为什么C#中的out泛型类型参数违反了协变性?

13
我不清楚为什么以下代码片段不是协变的?
  public interface IResourceColl<out T> : IEnumerable<T> where T : IResource {

    int Count { get; }

    T this[int index] { get; }

    bool TryGetValue( string SUID, out T obj ); // Error here?
    }

错误1:无效的方差:类型参数'T'必须在'IResourceColl.TryGetValue(string, out T)'上不变地有效。'T'是协变的。

我的接口只在输出位置使用模板参数。我可以轻松地将此代码重构为以下内容:

  public interface IResourceColl<out T> : IEnumerable<T> where T : class, IResource {

    int Count { get; }

    T this[int index] { get; }

    T TryGetValue( string SUID ); // return null if not found
    }

但是我正在努力理解我的原始代码是否实际上违反了协变,或者这是协变的编译器或.NET限制。


这里需要注意的重要事情是,out(参数修饰符)与 out(用于泛型类型参数)完全不相关。 - Jon
@Jon - 这个问题适用于 C# 3.0 及之前的版本。这里描述的语法是 C# 4.0。 - Oded
值得指出的是,C#泛型不是模板。如果你来自C++背景,C++模板和C#泛型的行为方式并不相同。 - Anthony Pegram
@Jon,为什么它不相关?在T泛型参数上删除“out”是另一种“修复”错误的方法,因此两者显然与此错误有关。 - MerickOWA
@MerickOWA:我的观点是,这里的问题在于尝试使用变体“out”参数,这是行不通的,并且不仅仅是由于泛型参数的差异(请参见链接的问题)。回想起来,“无关”可能不是正确的词。 - Jon
显示剩余4条评论
4个回答

14
问题确实存在于此处:
bool TryGetValue( string SUID, out T obj ); // Error here?

你将obj标记为out参数,但这仍然意味着你正在传入obj,因此它不能是协变的,因为你既传入类型为T的实例,又返回它。
编辑:
Eric Lippert在他对“C#中的ref和out参数不能被标记为variant”的回答中表达得更好,并引用了他关于out参数的观点:
“T”标记为“out”是否合法?不幸的是不合法。 “out”实际上在内部与“ref”没有区别。 “out”和“ref”之间唯一的区别是编译器禁止在调用方分配之前从“out”参数中读取,并且编译器要求在调用方正常返回之前进行赋值。在.NET语言中编写了此接口的实现的人将能够在初始化之前从该项中读取,因此它可以用作输入。因此,在这种情况下,我们禁止将T标记为“out”。这是令人遗憾的,但我们无法做任何事情;我们必须遵守CLR的类型安全规则。

我可能会传递一个除T以外的实例,但是C#规则要求在读取之前必须先赋值,因此它永远不会引起问题。 - MerickOWA
啊,我现在明白了,在其他语言中读取值是可能的,而Eric的博客指出即使在C#中也有可能“破坏”。这就解释了为什么会违反协变性。 - MerickOWA
问题在于所谓的“out”参数实际上并不是真正的“out”参数;一个真正的“out”参数会导致编译器为函数的返回生成一个结构体类型,其中包括指定的返回类型和所有的“out”参数;调用者将自动从结构体中复制适当的字段到“out”参数中,然后将剩余的字段(如果有)视为返回值。如果“out”参数以这种方式实现,它们确实可以是协变的。 - supercat
1
@MerickOWA 如果你不理解接口和泛型协变,也可以这样理解。考虑C#版本1.2。创建一个方法void MyMethod(out Dog dog) { /* method body */}。为什么不能像这样调用该方法:Animal a; MyMethod(out a); 原因是一样的。MyMethod可能会进入一个Animal字段。然后调用一个将同一Animal字段设置为new Cat()的方法。然后说dog.Bark();。(这只是Jon Skeet在此答案中链接的最后一个示例的模仿。) - Jeppe Stig Nielsen

2

以下是使用扩展方法的可能解决方法。从实现者的角度来看,不一定方便,但用户应该会很满意:

public interface IExample<out T>
{
    T TryGetByName(string name, out bool success);
}

public static class HelperClass
{
    public static bool TryGetByName<T>(this IExample<T> @this, string name, out T child)
    {
        bool success;
        child = @this.TryGetByName(name, out success);
        return success;
    }
}

public interface IAnimal { };

public interface IFish : IAnimal { };

public class XavierTheFish : IFish { };

public class Aquarium : IExample<IFish>
{
    public IFish TryGetByName(string name, out bool success)
    {
        if (name == "Xavier")
        {
            success = true;
            return new XavierTheFish();
        }
        else
        {
            success = false;
            return null;
        }
    }
}

public static class Test
{
    public static void Main()
    {
        var aquarium = new Aquarium();
        IAnimal child;
        if (aquarium.TryGetByName("Xavier", out child))
        {
            Console.WriteLine(child);
        }
    }
}

1

它违反了协变性,因为提供给输出参数的值必须与输出参数声明的类型完全相同。例如,假设T是一个字符串,协变性意味着这样做是可以的:

var someIResourceColl = new someIResourceCollClass<String>();
Object k;
someIResourceColl.TryGetValue("Foo", out k); // This will break because k is an Object, not a String

1

研究这个小例子,你就会明白为什么它不被允许:

public void Test()
{
    string s = "Hello";
    Foo(out s);
}

public void Foo(out string s) //s is passed with "Hello" even if not usable
{
    s = "Bye";
}

out 意味着在方法执行离开之前必须明确分配 s,反之,在方法体中必须先定义 s 才能使用它。这似乎与 协变规则 兼容。但在调用方法之前,你可以在调用站点分配 s,并将该值传递给方法,这意味着即使它不可用,你仍然有效地传递了一个已定义类型的参数到该方法中,这违反了协变 规则,指出通用类型只能用作方法的返回类型。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接