在C#中分配此关键字

时间:2012-04-06 02:54:03

标签: c# .net memory specifications

主要问题是允许在有用性和内存方面修改this关键字的含义是什么;为什么在C#语言规范中允许这样做?

如果选择这样做,可以回答其他问题/子部分。我认为对它们的回答有助于澄清主要问题的答案。

我将此作为What's the strangest corner case you've seen in C# or .NET?

的答案
public struct Teaser
{
    public void Foo()
    {
        this = new Teaser();
    }
}

我一直试图解决为什么C#语言规范甚至会允许这样做。 子部分1 。是否有任何理由让这个可以修改?这一切都有用吗?

对该答案的评论之一是

  

从CLR到C#:他们之所以这样做是因为你   可以在另一个中调用struct的无参数构造函数   构造函数。如果你只想初始化一个结构的一个值和   希望其他值为零/ null(默认),您可以写公开   Foo(int bar){this = new Foo(); specialVar = bar;}。这不是   高效且不合理(specialVar被分配两次),但是   仅供参考。 (这就是书中给出的理由,我不知道为什么   不应该只做公共Foo(int bar):this())

子部分2。我不确定我是否遵循这个推理。有人可以澄清他的意思吗?也许是一个如何使用它的具体例子?

编辑(忽略堆栈或堆主要点是关于内存释放或垃圾收集。而不是int []你可以用262144 public int字段替换它 另外从我的理解结构是在堆栈上创建而不是堆,如果这个结构有一个1 Mb字节数组字段初始化为

public int[] Mb = new int[262144];

子部分3。在调用Foo时是否会从堆栈中删除?对我来说,似乎因为结构永远不会超出范围,所以它不会从堆栈中删除。今晚没有时间来创建一个测试用例,但也许明天我会为此做一个。

在下面的代码中

Teaser t1 = new Teaser();
Teaser tPlaceHolder = t1;
t1.Foo();

子部分4 。 t1和tPlaceHolder是否占用相同或不同的地址空间?

很抱歉提到一个3岁的帖子,但这个确实让我头疼。

仅供参考关于stackoverflow的第一个问题,所以如果我对这个问题有问题,请发表评论然后我会编辑。

2天之后,即使我已经选择了一位获胜者,我也会在这个问题上投入50分,因为我认为答案需要合理的工作来解释问题。

3 个答案:

答案 0 :(得分:8)

首先,我认为你应该首先检查一下你是否提出了正确的问题。也许我们应该问,“为什么C#允许在结构中分配this?”

在引用类型中分配this关键字是有潜在危险的:您正在覆盖对正在运行的方法的对象的引用;你甚至可以在初始化该引用的构造函数中这样做。尚不清楚应该是什么行为。为避免弄清楚,因为它通常不常用,所以规范(或编译器)不允许这样做。

但是,在值类型中分配this关键字是明确定义的。值类型的赋值是复制操作。每个字段的值以递归方式从赋值的右侧到左侧复制。这对结构来说是一个非常安全的操作,即使在构造函数中也是如此,因为结构的原始副本仍然存在,您只是在更改其数据。它完全等同于手动设置结构中的每个字段。为什么规范或编译器禁止定义明确且安全的操作?

顺便说一下,这回答了你的一个子问题。值类型分配是深层复制操作,而不是引用副本。鉴于此代码:

Teaser t1 = new Teaser();
Teaser tPlaceHolder = t1;
t1.Foo();

您已分配了Teaser结构的两个副本,并将第一个字段的值复制到第二个字段中。这是值类型的本质:两个具有相同字段的类型是相同的,就像两个包含10个的int变量一样,无论它们在“内存中”的哪个位置都是相同的。

此外,这很重要且值得重复:仔细考虑“堆栈”与“堆”的内容。值类型始终在堆上结束,具体取决于它们的使用环境。没有关闭或以其他方式从其范围中提升的短期(本地范围)结构很可能被分配到堆栈中。但这是一个不重要的implementation detail ,你既不应该关心也不应该依赖。关键是它们是值类型,并且表现如此。

至于对this的分配有多么有用:不是很好。已经提到了具体的用例。您可以使用它来大多初始化具有默认值的结构,但指定一个小数字。由于您需要在构造函数返回之前设置所有字段,因此可以节省大量冗余代码:

public struct Foo
{
  // Fields etc here.

  public Foo(int a)
  {
    this = new Foo();
    this.a = a;
  }
}

它也可用于执行快速交换操作:

public void SwapValues(MyStruct other)
{
  var temp = other;
  other = this;
  this = temp;
}

除此之外,它只是语言的一个有趣的副作用以及结构和值类型的实现方式,您很可能永远不需要知道。

答案 1 :(得分:1)

具有此可分配允许带有结构的“高级”边角情况。我找到的一个例子是交换方法:

struct Foo 
{
    void Swap(ref Foo other)
    {
         Foo temp = this;
         this = other;
         other = temp;
    }
}

我强烈反对这种用法,因为它违反了不可变性的结构的默认“期望”性质。有这个选择的原因可以说是不清楚的。

现在谈到结构自己。他们在几个方面与班级不同:

  • 它们可以存在于堆栈而不是托管堆中。
  • 可以将它们封送回非托管代码。
  • 无法将它们分配给NULL值。

有关完整概述,请参阅:http://www.jaggersoft.com/pubs/StructsVsClasses.htm

相对于你的问题,你的结构是存在于堆栈还是堆上。这由结构的分配位置决定。如果struct是类的成员,它将在堆上分配。否则,如果直接分配结构,它将被分配在堆上(实际上这只是图片的一部分。一旦开始讨论C#2.0中引入的闭包,这整个将变得相当复杂但是现在它足以使回答你的问题)。

.NET中的数组是在堆上默认分配的(当使用不安全的代码和stackalloc关键字时,这种行为不一致)。回到上面的解释,这表明结构实例也在堆上分配。实际上,证明这一点的一种简单方法是分配一个大小为1 mb的数组,并观察如何抛出NO stackoverflow异常。

堆栈上实例的生命周期由其范围决定。这与管理器堆上的实例不同,该实例的生存期由垃圾收集器确定(以及是否仍存在对该实例的引用)。只要在范围内,您就可以确保堆栈中的任何内容都存在。在堆栈上分配实例并调用方法不会释放该实例,直到该实例超出范围(默认情况下,当声明该实例的方法结束时)。

一个struct cant已经管理了对它的引用(指针在非托管代码中是可能的)。在C#中使用堆栈上的结构时,基本上只有一个实例而不是引用的标签。将一个结构分配给另一个结构只是复制基础数据。您可以将引用视为结构。天真地说,引用只不过是一个包含指向内存中某个部分的指针的结构。将一个引用分配给另一个引用时,将复制指针数据。

// declare 2 references to instances on the managed heap
var c1 = new MyClass();
var c2 = new MyClass();

// declare 2 labels to instances on the stack
var s1 = new MyStruct();
var s2 = new MyStruct();

c1 = c2; // copies the reference data which is the pointer internally, c1 and c2 both point to the same instance
s1 = s2; // copies the data which is the struct internally, c1 and c2 both point to their own instance with the same data

答案 2 :(得分:0)

您可以利用 this 并改变不可变结构

public struct ImmutableData
{
    private readonly int data;
    private readonly string name;

    public ImmutableData(int data, string name)
    {
        this.data = data;
        this.name = name;
    }

    public int Data { get => data; }
    public string Name { get => name; }

    public void SetName(string newName)
    {
        // this wont work
        // this.name = name; 

        // but this will
        this = new ImmutableData(this.data, newName);
    }

    public override string ToString() => $"Data={data}, Name={name}";
}

class Program
{
    static void Main(string[] args)
    {
        var X = new ImmutableData(100, "Jane");
        X.SetName("Anne");

        Debug.WriteLine(X);
        // "Data=100, Name=Anne"
    }
}

这是有利的,因为您可以实现IXmlSerializable并保持不可变结构的健壮性,同时允许序列化(一次发生一个属性)。

在上面的示例中只有两种方法可以实现此目的:

    public void ReadXml(XmlReader reader)
    {
        var data = int.Parse(reader.GetAttribute("Data"));
        var name = reader.GetAttribute("Name");

        this = new ImmutableData(data, name);
    }
    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("Data", data.ToString());
        writer.WriteAttributeString("Name", name);
    }

创建后续的xml文件

<?xml version="1.0" encoding="utf-8"?>
<ImmutableData Data="100" Name="Anne" />

,可以通过

读取
        var xs = new XmlSerializer(typeof(ImmutableData));
        var fs = File.OpenText("Store.xml");
        var Y = (ImmutableData)xs.Deserialize(fs);
        fs.Close();