主要问题是允许在有用性和内存方面修改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分,因为我认为答案需要合理的工作来解释问题。
答案 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;
}
}
我强烈反对这种用法,因为它违反了不可变性的结构的默认“期望”性质。有这个选择的原因可以说是不清楚的。
现在谈到结构自己。他们在几个方面与班级不同:
有关完整概述,请参阅: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();