【问题标题】:Details on what happens when a struct implements an interface有关结构实现接口时发生的情况的详细信息
【发布时间】:2013-03-04 17:21:37
【问题描述】:

我最近遇到了这个 Stackoverflow 问题:When to use 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!?

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

【问题讨论】:

  • 我想你自己回答了这个问题——“结构变成了引用类型并被移到了堆中
  • 我不知道你的例子中的 Foobar 是什么...
  • @JoshE 所以两个引用会指向同一个东西?如果值类型发生变化怎么办?是否有任何 MSDN 文档或解释这一点的东西?
  • 它被称为拳击,MSDN 上有很多文档。 msdn.microsoft.com/en-us/library/yz2be5wk.aspx你发布的代码测试了吗?
  • 想一想:如果我尝试Foo foo = b.Biz;,我会得到一个“无法将类型 IFoo 隐式转换为 Foo”的编译错误 - 你必须显式地装箱/拆箱,这就是 foo = null 的原因和方式值类型不能是null(指向堆上的值类型的指针 = null)

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


【解决方案1】:

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

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

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

【讨论】:

  • 你应该以某种方式突出显示最后一段,这非常重要。
  • @supercat 是一个通用参数并不是导致未装箱接口调用的唯一场景。如果某个值类型实现了IDisposable,然后在using 语句中实例化(如using (someclass.GetDisposableStruct()) { ; }),那么编译器就足够智能以发出constrained.<Type> CIL 指令。我猜IEnumerable.GetEnumerator()foreach 中的值类型上调用时也会发生同样的情况。
  • @supercat - 你是说,无论如何我通过接口(在给定的情况下为 IFoo)访问结构,结构实际上被转换为堆对象?
  • @jasnevo:如果您使用接口类型的变量,而不是受接口约束的泛型,该变量将能够保存null 或对存储在堆上的对象的引用.将结构存储到此类变量中的尝试将在堆上创建一个对象并存储对该对象的引用。
【解决方案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

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

【讨论】:

  • 重要的是要注意,将结构存储在接口类型的位置会将其值复制到将按照引用语义运行的堆对象,但将其作为受约束的泛型传递则不会。一些像IEquatable<T> 这样的接口的存在是为了在没有装箱的情况下被调用。
  • @supercat 同意。如果我有void MyMethod<T>(T t) { ... },那么如果T 是值类型,那么t 当然不会被装箱。如果我添加一个像where T : IFace 这样的约束,当然它仍然不会框t。但是,如果有约束,我在方法体内说t.MemberOfIFace();,由于我的约束,这将是合法的,我猜它将不得不导致 在那一点装箱。
  • 不。 .NET 中泛型的一大优点是,如果一个泛型类型参数被限制在一个接口上,则可以在该类型的事物上使用接口成员而无需装箱。即时编译器将为适用于它的每个不同的通用值类型或其组合编译方法的单独版本。对于任何给定的结构类型 T,JIT 可以确定应该调用什么方法并生成直接调用该方法的代码,而无需虚拟调度的对象头。
  • 我认为不幸的是,没有很好的方法可以在语法上表明一个人希望在一个结构上使用一个接口成员,该结构已知实现该接口而不装箱。如果接口IFoo 有成员int Bar(string),则可以编写一个静态方法static int CallIFooBar<T>(ref T it, string param) where T:IFoo { return it.Bar(param); } 并使用它来调用接口方法Bar 而无需装箱,但这相当笨拙。
  • @supercat 听起来不错。那么即使MemberOfIFace 是用显式接口实现编写的,该方法也会在没有首先“强制转换”(装箱)t 变量以键入IFace 的情况下被调用?凉爽的。 (我指的是你的第二条评论。)
【解决方案3】:

我在 2013 年 3 月 4 日回复你关于你的实验的帖子,虽然我可能有点晚了 :)

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

对于行为 1,您具有 IFoo 类型的 Biz auto 属性,因此当您在此处设置值时,它将被装箱并且该属性将保留对该框的引用。每当您获得属性的值时,都会返回该框。这样一来,它的工作原理就好像 Foo 是一个类一样,你会得到你所期望的:你设置一个值,然后把它取回。

现在,使用行为 2,您存储了一个结构(字段 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,所以这仍然是您在该框中的内容对于该方法的其余部分。

【讨论】:

    【解决方案4】:

    所以,我决定亲自测试一下这种行为。我会给出“结果”,但我无法解释为什么会这样。希望对它的工作原理有更多了解的人能来并以更彻底的答案启发我

    完整的测试程序:

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

    如您所见,通过手动装箱/拆箱,我们会得到非常不同的行为。不过,我并不完全理解这两种行为。

    【讨论】:

    • 在 99% 的情况下,如果您认为堆类型的变量将持有对堆实例的引用,并且将一种堆类型的引用转换为对另一个将产生对 same 实例的引用;将值类型转换为堆类型,反之亦然,将复制相关数据。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多