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

时间:2013-03-04 17:21:38

标签: c# .net struct value-type boxing

我最近遇到了这个Stackoverflow问题:When to use struct?

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

  

另外,要意识到当一个struct实现一个接口时 - 就像   枚举器做 - 并且被强制转换为实现的类型struct   成为引用类型并移动到堆。内部的   字典类,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!?

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

4 个答案:

答案 0 :(得分:12)

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

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

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

答案 1 :(得分:2)

了解拳击取消装箱(搜索互联网)。例如MSDN:Boxing and Unboxing (C# Programming Guide)

另请参阅SO线程Why do we need boxing and unboxing in C#?以及链接到该线程的线程。

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

object obj = new Foo(); // boxing

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

IFoo iFoo = new Foo(); // boxing

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

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

答案 2 :(得分:0)

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

完整的测试程序:

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
        }
    }
}

正如您所看到的,通过手动装箱/取消装箱,我们得到了极其不同的行为。我不完全理解这两种行为。

答案 3 :(得分:0)

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

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

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

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

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

E.g。在行

    b.Biz.Foobar=567;

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

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

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