14

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

在里面,它有一个回答有点深奥:

此外,要意识到当一个结构实现了一个接口 - 就像 Enumerator 所做的那样 - 并被强制转换为该实现的类型时,该结构成为一个引用类型并被移动到堆中。在 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!?

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

4

4 回答 4

15

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

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

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

于 2013-03-04T20:15:35.667 回答
2

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

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

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

object obj = new Foo(); // boxing

或“转换”为已实现的接口,如

IFoo iFoo = new Foo(); // boxing

唯一的基类 astruct有、是System.ValueTypeobject(包括dynamic)。类型的基类enumSystem.EnumSystem.ValueTypeobject

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

于 2013-05-14T10:26:17.710 回答
1

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

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

对于行为 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,所以这仍然是您在该框中的其余部分的方法。

于 2017-09-27T12:47:17.223 回答
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
        }
    }
}

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

于 2013-03-04T19:27:21.087 回答