在 C# 中分配此关键字

本文关键字:关键字 分配 | 更新日期: 2023-09-27 18:30:25

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

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

我遇到了这个作为您在 C# 或 .NET 中看到的最奇怪的极端情况的答案?

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

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

对这个答案的评论之一是

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

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

编辑(忽略堆栈或堆的要点是关于内存释放或垃圾回收。 代替 int[],您可以用 262144 个公共 int 字段替换它)同样根据我的理解,如果此结构要初始化一个 1 Mb 字节的数组字段,则在堆栈上创建结构而不是堆

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

子部分 3. 调用 Foo 时,是否会将其从堆栈中删除? 对我来说,似乎由于结构从未超出范围,因此不会从堆栈中删除。 今晚没有时间创建一个测试用例,但也许我明天会为这个。

在下面的代码中

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

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

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

仅供参考,关于堆栈溢出的第一个问题,因此如果我对这个问题有问题,请发表评论,我会编辑。

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

在 C# 中分配此关键字

首先,我认为你应该从检查你是否问了正确的问题开始。也许我们应该问,"为什么 C# 不允许在结构中赋值this

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

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

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

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

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

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

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

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

具有此可分配对象允许具有结构的"高级"极端情况。我发现的一个例子是交换方法:

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 的数组,并观察 NO 堆栈溢出异常是如何抛出的。

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

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

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

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();
我在

查找System.Guid是如何实现时遇到了这个问题,因为我有一个类似的场景。

基本上,它这样做(简化):

struct Guid
{
    Guid(string value)
    {
        this = Parse(value);
    }
}

我认为这是一个非常简洁的解决方案。