为什么在两个看似完全不同的上下文中使用'out'关键字?


11

在C#中,out可以两种不同的方式使用关键字。

  1. 作为参数修饰符,其中参数通过引用传递

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
    
  2. 作为类型参数修饰符,以指定协方差

    // Covariant interface. 
    interface ICovariant<out R> { }
    
    // Extending covariant interface. 
    interface IExtCovariant<out R> : ICovariant<R> { }
    
    // Implementing covariant interface. 
    class Sample<R> : ICovariant<R> { }
    
    class Program
    {
        static void Test()
        {
            ICovariant<Object> iobj = new Sample<Object>();
            ICovariant<String> istr = new Sample<String>();
    
            // You can assign istr to iobj because 
            // the ICovariant interface is covariant.
            iobj = istr;
        }
    }
    

我的问题是:为什么?

对于初学者而言,两者之间的联系似乎并不直观。泛型的使用似乎与通过引用传递没有任何关系。

我首先了解了out与通过引用传递参数有关的内容,这阻碍了我对使用泛型定义协方差的理解。

我所缺少的这些用途之间是否有联系?


5
如果您看一下System.Func<in T, out TResult>委托中的协方差和逆方差用法,则该连接稍微容易理解。
rwong 2015年

4
此外,大多数语言设计人员都试图尽量减少关键字的数量,并且在某些现有语言中使用大型代码库来添加新关键字会很痛苦(可能与使用该单词作为名称的某些现有代码发生冲突)
Basile Starynkevitch 2015年

Answers:


20

有连接,但是有点松动。在C#中,关键字“ in”和“ out”作为其名称建议代表输入和输出。对于输出参数,这非常清楚,但是与模板参数的关系则不太清楚。

让我们看一下Liskov替代原理

...

Liskov的原理对签名提出了一些标准要求,这些签名已在较新的面向对象的编程语言中采用(通常在类级别而不是类型上;有关区别,请参见名义与结构子类型):

  • 子类型中方法参数的矛盾性。
  • 子类型中返回类型的协方差。

...

看看协方差与输入如何关联,协方差与输出如何关联?在C#中,如果您使用标记模板变量out以使其协变,但是请注意,仅当提到的type参数仅作为输出(函数返回类型)出现时,您才能执行此操作。因此以下内容无效:

interface I<out T>
{
  void func(T t); //Invalid variance: The type parameter 'T' must be
                  //contravariantly valid on 'I<T>.func(T)'.
                  //'T' is covariant.

}

类似地,如果使用标记类型参数in,则意味着只能将其用作输入(功能参数)。因此以下内容无效:

interface I<in T>
{
  T func(); //Invalid variance: The type parameter 'T' must
            //be covariantly valid on 'I<T>.func()'. 
            //'T' is contravariant.

}

综上所述,与out关键字的联系是与功能参数一起表示它是输出参数,而对于类型参数则表示类型仅在输出上下文中使用。

System.Func也是rwong在评论中提到的一个很好的例子。在System.Func所有输入参数中,ar标记为in,而输出参数标记为out。原因正是我所描述的。


2
好答案!救了我一些……等一下……打字!顺便说一句:您引用的LSP部分实际上早于Liskov就已知道。这只是函数类型的标准子类型化规则。(参数类型是协变的,返回类型是协变的)。Liskov方法的新颖之处在于:a)以协方差/反方差而是以行为替代性(由前置条件/​​后置条件定义)来表述规则,并且b)历史规则,这使得可以应用所有这种推理可变的数据类型,这以前是不可能的。
约尔格W¯¯米塔格

10

@Gábor已经解释了这种联系(所有“输入”的内容都具有协方差,所有“输出”的内容均具有协方差),但是为什么还要重用关键字呢?

好吧,关键字非常昂贵。您不能在程序中将它们用作标识符。但是英语中只有那么多单词。因此,有时您会遇到冲突,并且必须笨拙地重命名变量,方法,字段,属性,类,接口或结构,以避免与关键字冲突。例如,如果您要对一所学校进行建模,那么您怎么称呼班级?您不能将其称为类,因为它class是一个关键字!

语言中添加关键字的成本甚至更高。基本上,所有使用此关键字作为标识符的代码都将变为非法,从而在各处破坏了向后兼容。

inout关键字已经存在,所以他们可能只是重复使用。

他们可以添加上下文关键字,它们只是类型参数列表上下文中的关键字,但是他们会选择哪些关键字?covariantcontravariant+-(例如,像Scala一样)?superextends像Java那样?您能记住那些参数是协变和逆变的吗?

在当前解决方案中,有一个不错的助记符:输出类型参数获取out关键字,输入类型参数获取in关键字。请注意,方法参数具有很好的对称性:输出参数获取out关键字,输入参数获取in关键字(嗯,实际上,根本没有关键字,因为输入是默认值,但是您可以理解)。

[注意:如果您查看编辑历史记录,您会发现我实际上是在介绍性句子中将这两者原本切换了。在那段时间,我什至获得了赞誉!这只是向您显示该助记符的真正重要性。]


记住协方差与逆方差的方法是考虑接口中的函数采用通用接口类型的参数会发生什么。如果一个具有interface Accepter<in T> { void Accept(T it);};,则an Accepter<Foo<T>>将接受T作为输入参数,如果将其接受作为Foo<T>输出参数,反之亦然。因此,方差。相比之下,interface ISupplier<out T> { T get();};a Supplier<Foo<T>>将具有任何类型的方差Foo-因此具有方差。
超级猫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.