16

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

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

我在回答你在 C# 或 .NET 中看到的最奇怪的极端情况是什么?

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

我一直试图弄清楚为什么 C# 语言规范甚至允许这样做。 第 1 部分。有什么可以证明是可修改的吗?都是有用的吗?

对该答案的评论之一是

从 CLR 通过 C#:他们这样做的原因是因为您可以在另一个构造函数中调用结构的无参数构造函数。如果您只想初始化结构的一个值而希望其他值为零/空(默认),您可以编写 public Foo(int bar){this = new Foo(); 特殊变量 = 酒吧;}。这效率不高,也不是很合理(specialVar 分配了两次),但仅供参考。(这就是书中给出的原因,我不知道为什么我们不应该只做 public Foo(int bar) : this())

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

编辑(忽略堆栈或堆的要点是关于内存释放或垃圾收集。您可以用 262144 个公共 int 字段代替 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,因为我认为答案需要合理的工作量来解释这些问题。

4

3 回答 3

12

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

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

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

顺便说一句,这回答了您的一个子问题。值类型赋值是深拷贝操作,而不是引用拷贝。鉴于此代码:

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

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

此外,这很重要并且值得重复:仔细假设“堆栈”与“堆”上的内容。值类型最终始终在堆上,具体取决于使用它们的上下文。没有关闭或以其他方式解除其范围的短期(本地范围)结构很可能被分配到堆栈上。但这是一个不重要的实现细节,您既不应该关心也不应该依赖它。关键是它们是值类型,并且行为如此。

至于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;
}

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

于 2012-04-06T04:09:16.110 回答
1

拥有这个可分配允许具有结构的“高级”极端情况。我发现的一个例子是一种交换方法:

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

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

现在谈到自己的结构。它们在以下几个方面与类不同:

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

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

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

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

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

一个结构不能有对它的托管引用(指针在非托管代码中是可能的)。在 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
于 2012-04-06T03:41:12.150 回答
0

您可以利用这一点并改变一个不可变的结构

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();
于 2019-12-19T21:16:54.523 回答