接口变量具有值类型还是引用类型语义?

18

接口变量具有值类型还是引用类型语义?

接口由类型实现,而这些类型既可以是值类型也可以是引用类型。显然,intstring 都实现了 IComparable 接口,其中 int 是值类型,string 是引用类型。但对于下面的情况呢:

IComparable x = 42;
IComparable y = "Hello, World!";

(我试图回答的问题可能已经被删除,因为它问接口是存储在堆栈还是堆上,而我们都应该知道,更有建设性的方法是从语义的角度考虑值类型和引用类型之间的差异,而不是它们的实现。关于此讨论,请参见Eric Lippert的The stack is an implementation detail。)


一个简单的在SO上的搜索就可以得到以下链接回答了同样的问题,还有很多其他的答案: https://dev59.com/tWsz5IYBdhLWcg3wTl0Thttps://dev59.com/jnA75IYBdhLWcg3wy8Ui#3101955https://dev59.com/5G025IYBdhLWcg3w25yy#5757359 - Rune FS
@RuneFS 在我发帖之前,我看到了那些问题。第一个链接讨论了一般的装箱问题;其中七个例子只有一个涉及将接口引用赋值给值类型的情况。第二个链接问是否将类实例分配给接口构成了装箱,这并没有解答关于值类型的部分问题。 - phoog
@RuneFS 没有一个问题是重复的(只是相关的)。 - nawfal
@phoog 这是一个更相关的为什么.NET中的接口是引用类型 - nawfal
显示剩余13条评论
4个回答

20
通常情况下,根据现有的答案,它是一种引用类型并需要装箱;但总会有例外。在一个带有where限制的泛型方法中,它可以既是引用类型也可以不需要装箱。
void Foo<T>(T obj) where T : ISomeInterface {
    obj.SomeMethod();
}

这是一项受限制的操作,即使它是值类型,也不会装箱。这是通过constrained实现的。对于引用类型,JIT将执行虚拟调用,对于值类型,将执行静态调用。没有装箱。


1
@phoog确实,它是一个介于两者之间的奇特层级-但在我看来,它是一个重要的方面,在问题标题的背景下应该被提到。 - Marc Gravell
1
@supercat 嗯...它们不会真正展示引用语义...具体的例子是什么? - Marc Gravell
1
@supercat 不,那只是幌子;你谈论的是一个不相关对象(TheList)的语义,与结构体的语义无关。仅在以第一阶变化的值方面谈论语义才有意义,即改变实体的,例如改变TheList - Marc Gravell
1
@MarcGravell:我的观点是,一个包含对类实例的私有不可变引用的结构体,在语义上更像它包装的类类型而不是值类型。更改上面的ListWrapStruct,使得索引setter调用包装列表的索引setter。将ListWrapStruct<int>的元素0设置为3,复制该结构,将副本的元素0更改为8,并读取原始的元素0。如果ListWrapStruct<int>具有值语义,则原始值仍然为3。但它将读取8。原始示例并不是最好的... - supercat
1
因为语义上它与外部列表相关联。但是稍微改变一下,使得带有 int 参数的构造函数创建并包装一个新的 List 实例,该实例在其他地方不会被公开。从语义上讲,结构体的行为类似于 List,并且复制结构体的行为类似于复制对 List 的引用。 - supercat
显示剩余10条评论

5
这是关于理解类型的装箱和拆箱。在你的例子中,int 在赋值时被装箱,并且一个指向该“盒子”或对象的引用被赋给了 x。值类型 int 被定义为实现 IComparable 的结构体。然而,一旦你使用接口引用来引用值类型 int,它将被装箱并放置在堆上。这就是它在实践中的工作方式。使用接口引用导致装箱发生的事实,根据定义使得这个引用具有引用类型语义。

MSDN:装箱和拆箱


“int”这个值类型并不是一个类,因此它没有实现任何接口。请查看文档:http://msdn.microsoft.com/en-us/library/system.int32.aspx,其中明确指出了“Int32”结构体实现了多个接口,包括“IComparable”、“IFormattable”、“IConvertible”、“IComparable<int>”和“IEquatable<int>”。 - phoog
1
你有一点正确 - int === struct Int32。除非将其转换为其中一个实现接口之一,否则没有区别,此时它变成了装箱对象。 - Eben Geer
2
phoog是正确的。仅有类实现接口的想法是完全错误的。结构体和数组不是类,但它们也实现了接口。(当然,接口也可以继承其他接口。) - Eric Lippert
是的,我看到我说的部分完全是错误的。我会再次编辑我的答案以更正。 - Eben Geer

5

一个类型为IComparable的变量或字段是引用类型变量或字段,无论该字段分配的值的类型如何。这意味着样例代码中的x被装箱了。

一个简单的测试可以证明这一点。这个测试基于这样一个事实:你只能将值类型拆箱为其原始类型(和该类型的可空版本)

    [TestMethod, ExpectedException(typeof(InvalidCastException))]
    public void BoxingTest()
    {
        IComparable i = 42;
        byte b = (byte)i;      //exception: not allowed to unbox an int to any type other than int
        Assert.AreEqual(42, b);
        Assert.Fail();
    }

编辑

此外,C#规范明确将引用类型定义为包括类类型、接口类型、数组类型和委托类型。

编辑2

正如Marc Gravell在他的回答中指出的那样,带有接口约束的泛型类型是一个不同的情况。这种情况不会导致装箱。


为什么你会想要发布一个问题,然后立即自己回答它呢? - Rune FS
@RuneFS Peter O.编辑了我的问题,删除了解释和链接http://meta.stackexchange.com/questions/17845/etiquette-for-answering-your-own-question。如果您单击显示“10分钟前编辑”或其他时间的时间,您可以查看版本历史记录--这是我几个月才偶然发现的事实。 - phoog
@RuneFS:因为,正如提问者最初所写的那样,“鼓励提出你已知答案的问题”。(http://meta.stackexchange.com/q/17845/156890)。 - Peter O.
1
实际上,还有其他的例外情况;例如,您可以将枚举类型拆箱为基础类型(即,您可以直接将 enum Foo {} 拆箱为 int),尽管装箱知道它是一个 Foo,而不是一个 int - Marc Gravell

2
接口类型的变量始终具有不可变语义、可变引用语义或“奇怪”的语义(与正常的引用或值语义不同)。如果 variable1variable2 都声明为相同的接口类型,其中一个执行 variable2 = variable1,并且再也不对任何变量进行写入,则由 variable1 引用的实例始终无法与由 variable2 引用的实例区分开来(因为它们是同一实例)。
带有接口约束的泛型类型可能具有不可变语义、可变引用语义或“奇怪”的语义,但也可能具有可变值语义。如果未将接口记录为具有可变值语义,则可能会很危险。不幸的是,没有办法限制接口具有不可变语义或可变值语义(这意味着在执行 variable2 = variable1 后,不应该通过编写 variable2 或反之来更改 variable1)。可以添加“结构体”约束以及接口约束,但这将排除具有不可变语义的类,同时不排除具有引用语义的结构体。

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