结构实现接口时会发生什么的详细信息

我最近遇到了这个Stackoverflow问题: 何时使用struct?

在其中,它有一个答案,说了一些有点深刻的东西:

另外,要意识到当一个struct实现一个接口时 – 就像Enumerator那样 – 并且被强制转换为该实现的类型,struct会成为一个引用类型并被移动到堆中。 在Dictionary类的内部,Enumerator仍然是一个值类型。 但是,只要方法调用GetEnumerator(),就会返回引用类型的IEnumerator。

这究竟是什么意思?

如果我有类似的东西

struct Foo : IFoo { public int Foobar; } class Bar { public IFoo Biz{get; set;} //assume this is Foo } ... var b=new Bar(); var f=b.Biz; f.Foobar=123; //What would happen here b.Biz.Foobar=567; //would this overwrite the above, or would it have no effect? b.Biz=new Foo(); //and here!? 

究竟什么样的值类型结构的详细语义被视为引用类型?

结构类型的每个声明都在Runtime中声明了两种类型:值类型和堆对象类型。 从外部代码的角度来看,堆对象类型的行为类似于具有相应值类型的字段和方法的类。 从内部代码的角度来看,堆类型的行为就像它具有相应值类型的字段一样。

尝试将值类型ValueType转换为引用类型( ObjectValueTypeEnum或任何接口类型)将生成其对应堆对象类型的新实例,并返回对该新实例的引用。 如果尝试将值类型存储到引用类型存储位置,或将其作为引用类型参数传递,则会发生同样的情况。 一旦将值转换为堆对象,它将从外部代码的角度作为堆对象。

在没有首先将值类型转换为堆对象的情况下,可以使用值类型的接口实现的唯一情况是将其作为具有接口类型作为约束的generics类型参数传递。 在该特定情况下,可以在值类型实例上使用接口成员,而不必首先将其转换为堆对象。

阅读有关装箱拆箱 (搜索互联网)的信息。 例如MSDN: Boxing和Unboxing(C#编程指南) 。

另见SO线程为什么我们需要在C#中装箱和拆箱? ,以及链接到该线程的线程。

注意:如果您“转换”为值类型的基类,则不是那么重要,如

 object obj = new Foo(); // boxing 

或“转换”为已实现的界面,如

 IFoo iFoo = new Foo(); // boxing 

struct具有的唯一基类是System.ValueTypeobject (包括dynamic )。 enum类型的基类是System.EnumSystem.ValueTypeobject

结构可以实现任意数量的接口(但它不从其基类inheritance接口)。 枚举类型实现IComparable (非generics版本), IFormattableIConvertible因为基类System.Enum实现了这三个。

所以,我决定自己把这种行为用于测试。 我会给出“结果”,但我无法解释为什么会发生这种情况。 希望有更多关于它如何工作的知识的人可以出现并以更彻底的答案启发我

完整的测试程序:

 using System; namespace Test { interface IFoo { int Foobar{get;set;} } struct Foo : IFoo { public int Foobar{ get; set; } } class Bar { Foo tmp; //public IFoo Biz{get;set;}; //behavior #1 public IFoo Biz{ get { return tmp; } set { tmp = (Foo) value; } } //behavior #2 public Bar() { Biz=new Foo(){Foobar=0}; } } class MainClass { public static void Main (string[] args) { var b=new Bar(); var f=b.Biz; f.Foobar=123; Console.WriteLine(f.Foobar); //123 in both b.Biz.Foobar=567; / Console.WriteLine(b.Biz.Foobar); //567 in behavior 1, 0 in 2 Console.WriteLine(f.Foobar); //567 in behavior 1, 123 in 2 b.Biz=new Foo(); b.Biz.Foobar=5; Console.WriteLine(b.Biz.Foobar); //5 in behavior 1, 0 in 2 Console.WriteLine(f.Foobar); //567 in behavior 1, 123 in 2 } } } 

正如您所看到的,通过手动装箱/拆箱,我们获得了截然不同的行为。 我不完全理解这两种行为。

我在2013-03-04回复你关于你的实验的post,虽然我可能有点晚了:)

请记住这一点:每次将struct值分配给接口类型的变量(或将其作为接口类型返回)时,它都会被装箱。 可以想象它将在堆上创建一个新对象(盒子),并在那里复制 struct的 。 该框将被保留,直到您有一个参考,就像任何对象一样。

对于行为1,您具有IFoo类型的Biz auto属性,因此当您在此处设置值时,它将被装箱并且该属性将保留对该框的引用。 每当您获得该属性的值时,将返回该框。 通过这种方式,它的工作方式就好像Foo会成为一个类,并且你得到了你所期望的:你设置一个值然后你就可以得到它。

现在,对于行为2,您存储一个struct(字段tmp),并且您的Biz属性将其值作为IFoo返回。 这意味着每次调用get_Biz时,都会创建一个新框并返回

查看Main方法:每次看到b.Biz时,都是一个不同的对象(框)。 这将解释实际行为。

例如排队

  b.Biz.Foobar=567; 

b.Biz在堆上返回一个方框,你将其中的Foobar设置为576然后,因为你没有保留对它的引用,它会立即丢失给你的程序。

在下一行你写了b.Biz.Foobar,但是这个对b.Biz的调用将再次创建一个非常新的框,其中Foobar具有默认的0值,这就是打印的内容。

下一行,变量f之前也被一个b.Biz调用填充,它创建了一个新的盒子,但是你保留了对它的引用(f)并将它的Foobar设置为123,所以这仍然是你在那个盒子中的其余部分方法。