在 C# 中分配此关键字

Posted

技术标签:

【中文标题】在 C# 中分配此关键字【英文标题】:Assign this keyword in C# 【发布时间】:2012-04-19 18:47:38 【问题描述】:

主要问题是允许修改 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 部分。有什么可以证明 this 是可修改的吗?有用吗?

其中一个答案是

从 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];

Sub-part 3. 调用 Foo 时,它是否会从堆栈中删除?在我看来,由于结构从未超出范围,因此不会从堆栈中删除。今晚没有时间创建一个测试用例,但也许我明天会做这个。

在下面的代码中

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

第 4 小节。 t1 和 tPlaceHolder 占用相同还是不同的地址空间?

很抱歉提出一个 3 年前的帖子,但这个帖子真的让我头疼。

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

2 天后,即使我已经在脑海中选择了获胜者,我也会在这个问题上悬赏 50,因为我认为答案需要合理的工作量来解释这些问题。

【问题讨论】:

1 中的 4 个问题不适合 Stack Overflow。一个问题应该是一个问题。 @AnthonyPegram - 在这种情况下,我认为这是可以接受的,因为这些问题是对结构真正如何工作的主要问题的子问题 我很抱歉不知道如何将其全部改写为一个。我想的主要问题可以概括为允许修改 this 关键字的副作用是什么?问题 3-4 是我认为可能的副作用。如果除了我之外还有其他人有权编辑问题并认为他们可以更好地提出问题,请随时这样做。或者留下评论提出更好的问题,我将从头开始。 天哪,我看到警察再次出警的问题。这是一个完全合法且真实的问题,它有具体和客观的答案,即使它包括四个问题。 4.作业在 C# 中创建副本。因此tPlaceHoldert1是不同的存储位置。 【参考方案1】:

您可以利用 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();

【讨论】:

【参考方案2】:

首先,我认为您应该首先检查您是否提出了正确的问题。也许我们应该问,“为什么 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 表示在关闭之前尝试。明天某个时候我会尝试改写这个问题。 我决定投票给这个答案,而不是重新打开一个新答案。按值复制是它最终陷入困境的原因。我想在我的脑海中,我一直认为“this”是引用,并且在处理结构时没有将“this”视为指的是值类型。我什至没有想到在我制作的任何结构中都使用了 this 关键字。【参考方案3】:

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

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 的数组,并观察如何抛出没有 *** 异常。

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

一个结构不能有对它的托管引用(指针在非托管代码中是可能的)。在 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

【讨论】:

我猜无论调用 Foo 时它被复制到堆栈还是堆中,GC 最终都会清理与原始结构相关联的内存吗?只是为了澄清我说的是严格管理的代码。哦,我所说的 Foo 是指 Teaser 结构中的 Foo,而不是你的 Foo 结构。 GC 只会清理托管堆上分配的实例。基于堆栈的实例实际上并没有真正清理(因此,结构不能包含析构函数)。一旦超出范围,他们的标签就会变得无效。请参阅 eri​​c lippert 在这篇文章中的回答:***.com/questions/6441218/… 结构是值类型;它们永远不会被明确地垃圾收集。当值类型离开作用域时,值类型的内存返回给操作系统;如果它是局部变量或参数,则发生这种情况然后函数返回。如果它是一个字段,它会在其容器类被 GC 处理时发生。等等。 参考我用 N 个公共 int 字段替换 int[] 的编辑。当调用 t1.Foo 时,这些整数的内存是否会被清理/未分配/从堆栈中释放? +1 表示在关闭之前尝试。明天某个时候我会尝试改写这个问题。

以上是关于在 C# 中分配此关键字的主要内容,如果未能解决你的问题,请参考以下文章

Python中的广度优先和深度优先

从类中分解所有依赖项的最简单、最快的方法

IDEA中分析JVM堆导出文件heapdump-1591244153347.hprof文件

IDEA中分析JVM堆导出文件heapdump-1591244153347.hprof文件

10. 结构和枚举

C# 关键字 Visual Studio 2012