结构体和IDisposable

18

我想知道为什么它不能编译?

public static void Main(string[] args)
{
    using (MyStruct sss = new MyStruct())
    {

        sss.s = "fsdfd";// Cannot modify members of 'sss' because it is a 'using variable' 

        //sss.Set(12);    //but it's ok
    }
}

public struct MyStruct : IDisposable
{
    public int n;
    public string s;

    public void Set(int n)
    {
        this.n = n;
    }
    public void Dispose()
    {
        Console.WriteLine("dispose");
    }
}

更新:但它运行得完美。为什么?

public static void Main(string[] args)
        {

            using (MyClass sss = new MyClass())
            {
                sss.Field = "fsdfd"; 
            }


        }

public class MyClass:IDisposable {

    public string Property1 { get; set; }
    public string Field;
    public void Method1 (){}

    public void Dispose()
    {
        Console.WriteLine("dispose class");
    }
 }

15
如果我看到有人在结构体上实现“IDisposable”,我会开枪打人。 - leppie
12
@leppie:这将是随机的人还是实施它的人?知道这一点可能很有好处。 - Fredrik Mörk
3
@leppie 你应该解释一下为什么 :-) 我还是没有看到关于这个原因的解释。为什么使用类(class)就不同呢? - xanatos
在任何无法将其与不进行此操作的结构成员区分开来的环境中(包括.NET),除属性设置器之外会改变'this'的结构成员都是有害的,但这并不意味着公共结构字段是有害的。 结构字段具有特定的语义,与类语义不同,但更加自包含和可预测。 给定大小为100的Rectangle数组将保存100个不同的Rectangle实例。 保证。 如果Rectangle是一个类,则长度为100的数组可以容纳100个不同的实例,100个对一个实例的引用或介于两者之间的任何内容。 - supercat
4
我发现 struct : IDisposable 有一个相当合理的用途,那就是滥用 using() 以确保使用 HtmlTextWriter 写入的 XML/HTML 标记元素被正确关闭(structDispose 方法使用正确的标记名称调用 WriteCloseTag - 尽管我的结构体是不可变的)。这样可以生成更易读的代码,类似于呈现的 HTML,因为 using 的缩进方式就像它正在渲染的元素一样。C# 编译器不会将结构体装箱,因为 CIL 使用 ldloca.s+constrained+callvirt,但我希望它能像 foreach 一样使用鸭子类型。 - Dai
显示剩余4条评论
8个回答

32
有一些人链接了我的文章,它关于值类型的突变为什么是一个坏主意。虽然理解这些概念很重要,当理解为什么丢弃一个结构体是一个坏主意时,而且当你这样做时突变这个结构体更糟糕,但那并不是真正应该链接的文章。你想阅读的那篇文章详细解释了所有这些:http://ericlippert.com/2011/03/14/to-box-or-not-to-box/ 简单来说:“使用”会生成值类型的副本,因此您正在处置一个副本。这意味着您必须非常小心--如果该值是,比如操作系统句柄,则可能有许多该值的副本分布在内存中,您需要确保无论有多少副本,都只处置一次。
另请参见如果我的结构实现IDisposable,那么在using语句中使用它时是否会被装箱?

我很乐意承认,修改“this”但没有“告诉任何人”的结构体方法是有害的(不幸的是,没有一种标记这些方法以便在只读上下文中禁止使用的手段)。然而,我强烈反对结构体不应该有公共可变字段的观点;请参见https://dev59.com/0XRC5IYBdhLWcg3wAcM3以获取简单的代码示例。考虑以下问题:大小为100的Array<Point>中有多少个不同的Point实例?大小为100的Array<Dictionary>中有多少个不同的Dictionary实例? - supercat
1
(字典被选择为任意类类型 - 我知道它不是最好的例子)。如果someStructType有一个公共int字段I,并且StructArr是someStructType的数组,则“StructArr [3] .I + = 4;”对StructArr [4] .I会产生什么影响?如果someClassType有一个公共int字段I,并且ClassArr是someClassType的数组,则“ClassArr [3] .I + = 4;”对ClassArr [4] .I会产生什么影响?类肯定有其用处,但如果一个类型用于表示值,并且可变性会有所帮助,则可变值类型通常比可变类类型更合适。 - supercat
@supercat:我在你链接的帖子上发表了评论,简而言之:你所辩论的一切都可以通过不可变值类型来实现。为什么它们特别需要是可变的 - Ron Warholic
@Ron Warholic:使用不可变实体,如果想要改变其中任何一个字段,就必须重写所有字段。如果想要做的是更改结构的一个字段,我建议这样做比创建一个几乎所有字段都与第一个相匹配的新结构更清晰和更高效。通过重写所有内容来进行一次修改有何优势? - supercat
@Eric Lippert:我可以理解从编译器编写者的角度看,.net 中处理结构体的某些方面使得与结构体一起工作变得困难(特别是所有结构体都被认为是可装箱的事实,以及除非另有约束,否则泛型类型参数预期可以交替使用结构体和类)。然而,语言构造的工作通常不应该是为了让编译器编写者的生活变得容易--它应该是为了让那些必须编写和维护生产代码的人们的生活变得容易。 - supercat
显示剩余8条评论

3
类和结构体的情况其实是一样的,只是看到了不同的效果。
当你将类的示例更改为:
using (MyClass sss = new MyClass())
{
    sss = null;          // the same error
    sss.Field = "fsdfd"; // ok
}

在第一次分配时,您将收到相同的错误。

解释是:您不能更改(突变)使用变量。但对于适用于引用而不是实例的类。

教训是:不要使用结构体。特别是不要使用可变结构体。


2
@Alex:"mutable" 意味着可以改变(变异)的意思。一个能够改变其值的结构体被称为可变结构体,通常被认为是不好的东西。更多阅读:为什么可变结构体是邪恶的? - Fredrik Mörk

3
using语句的意义在于,当你离开代码块时,确保资源被正确处理。
当你把一个值类型变量赋值给其他对象时,实际上是用一个全新的同类型对象替换了原有对象。所以在此情况下,原来预定要被处理的对象已被丢弃。

另外,您将在仍在使用结构的另一个副本时处理未受管控的资源。砰! - leppie
在C#中,当使用字段/属性时,结构体是否采用写时复制? - xanatos
@xanatos:不,但是using(和foreach)结构会创建一个临时变量。使用类很好,因为这只是一个引用。 - leppie
@xanatos - 不是的。你是否在考虑C++中有时如何实现字符串?结构体只是可变对象,仅由引用它们的变量拥有。变量可以是不可变的(在这种情况下),结构体的作者可以通过readonly关键字将结构体中的个别字段标记为不可变。 - Daniel Earwicker
@DanielEarwicker 实际上,(真正的)问题是/应该是“为什么变量是不可变的”,而不是“为什么我不能使用字段”。有趣的是,你甚至不能使用属性 (考虑到属性实际上并不需要修改字段)。 - xanatos

2

请考虑以下内容:

 interface IFoo: IDisposable { int Bar {get;set;}}

 struct Foo : IFoo
 {
   public int Bar { get; set; }
   public void Dispose() 
   {
     Console.WriteLine("Disposed: {0}", Bar);
   }
 }

现在执行以下操作:
  IFoo f = new Foo();

  using (f)
  {
    f.Bar = 42;
  }

  Console.WriteLine(f.Bar); 

这将打印:

Disposed: 42
42

在这个例子中,使用已经接收到一个引用,因为你正在将IFoo f = new Foo()装箱。 - xanatos
@xanatos:确切地说,它展示了如何做到这一点,如果你真的想要的话;P - leppie
2
那就写出来吧!将值类型强制转换为接口会导致装箱(除了泛型约束的情况外),这对于75%的C#程序员来说并不明显 :-) - xanatos
@xanatos:我真的不想鼓励这个:) 但如果编码人员确切知道自己在做什么,那么这是可以接受的。 - leppie

2

终于我理解了 :-) 我会发表我的看法 :-) :-)

现在...

using (MyType something = new MyType())

等价于元
using (readonly MyType something = new MyType())

使用readonly关键字具有与类/结构声明中的readonly关键字相同的含义。

如果MyType是引用类型,那么被"保护"的是引用而不是引用的对象。因此,您不能执行以下操作:

using (readonly MyType something = new MyType())
    something = null;

但是你可以。
    something.somethingelse = 0;

在使用块中。

如果MyType是值类型,则readonly“修饰符”扩展到其字段/属性。因此,在using中,他们没有引入新类型的“const-ness / readonly-ness”,而是使用了他们已经拥有的。

因此,问题应该是:为什么我不能修改readonly值类型的字段/属性?

请注意,如果您这样做:

public void Dispose()
{
    Console.WriteLine("During dispose: {0}", n);
}

var sss = new MyStruct();

using (sss)
{
    sss.n = 12;
    Console.WriteLine("In using: {0}", sss.n); // 12
}

Console.WriteLine("Outside using: {0}", sss.n); // 12

结果

In using: 12
During dispose: 0
Outside using: 12

所以,using正在执行对sss的“私有”复制,而sss.n = 12正在访问“原始”的sss,而Dispose正在访问副本。

@leppie 我必须说实话,只读值类型的字段和属性是只读的这一事实并不是很“明显”。考虑到该值类型仍可通过方法进行更改,而属性更类似于方法而非字段,因此并没有任何真正“明显”的原因。而且两个“只读性”相同的事实也不是很明显。 - xanatos
readonly与此无关。这是结构体与类的语义问题。前者按值传递,后者按引用传递。如果您“引用”前者,则会复制该值,而后者则会复制引用(但仍指向原始对象)。 - leppie
@xanatos 我仍然不明白你是如何得到 "During dispose: 0" 的。我在这里进行了一个测试(http://i.stack.imgur.com/rCH6V.jpg),我并没有发现任何问题。 - Royi Namir
@RoyiNamir 你还没有理解这个例子 :-) 这里是完整的例子:http://ideone.com/F22MHw(我已经更改了值,以使其更清晰)。 0n 的默认值。该示例的重点是,在 using 中没有对您声明的 sss 进行 Dispose,而是对其副本进行处理,因为 MyStruct 是值类型。因此,如果在 using 中更改其值,则 Dispose 不会看到“新”值,因为它正在使用从“之前”值更改之前的 struct 的副本。 - xanatos
1
嗨,我来自未来,想说只读字段并不真正等同于元数据。只读结构体字段在调用方法时会创建防御性副本,而在using语句中创建的变量则不会,因此它们仍然可以通过调用它们的方法进行改变。此外,被改变的结构体将被处理。请自行查看:https://dotnetfiddle.net/EPjjCN - Oscar Abraham
显示剩余4条评论

1

这个答案关于C#是不正确的 - 结构体不能被重新分配,但它的内容可以被修改。但是可变的结构体本身就是一个糟糕的想法,所以可以说这个回答比语言本身更正确。 :) - Daniel Earwicker
我同意@DanielEarwicker的观点。 - leppie

1

我对这个不是100%确定,所以如果我错了,请任何人纠正我。

编译器允许你在这种情况下修改类的字段,但不能修改结构体的原因与内存管理有关。对于类(即引用类型),不是对象本身,而是引用本身是值。因此,当你修改该对象中的字段时,你实际上是在操作一个由该值引用的其他地方的内存块,而不是操作值本身。对于结构体,对象本身就是值,所以当你在结构体中操作字段时,实际上是在操作被视为只读的值。

编译器允许方法调用(进而修改字段)的原因很简单,它无法深入分析以确定该方法是否执行了这样的修改。

关于这个问题,在MS Connect上有一个(关闭的)案例,可能会提供更多信息:无法将IDisposable结构体的字段分配给using变量时出现CS1654错误


不太正确。看一下 using(和 foreach)生成的代码,两者都会创建一个临时变量并分配给它。对于结构体,该值会被复制。 - leppie
该操作者在结构体的副本上进行字段赋值。虽然从技术上讲,代码是有效的,但编译器会拒绝它,因为它没有任何效果(并且表示存在逻辑错误)。 - leppie
这解释了规则,但并未解释规则背后的原因。引入这个规则是为了解决什么问题? - xanatos
补充以上评论,通过一些技巧,您可以绕过这些限制。提示:装箱值类型。 - leppie
代码和IL在此处可用,如果有人想要检查:http://pastebin.com/GvbTNjyD - Fredrik Mörk
显示剩余6条评论

0
结构体被标记为只读。您正在尝试修改公共成员变量,编译器将其标记为只读并禁止修改。
但是,允许调用Set(),因为编译器无法知道调用是否会改变结构的状态。 实际上,这是一种巧妙的方法来修改只读值!
请参阅Eric Lippert的mutating readonly structs帖子了解更多信息。

这与“readonly”无关。字段从不隐式地是“readonly”。 - leppie
此外,使用 Set() 来改变 readonly 字段是行不通的。编译器会报错。 - leppie
@leppie:在Set()函数内,该字段不是只读的。然而,在示例中,由于它位于“using”块中,结构体实例是只读的。编译器从中推断出结构体上的任何公共字段也将是只读的。 - Sean
你错了。编译器不会推断任何这样的细节。除了在构造函数中,你不能改变一个readonly字段(注意,现在不是讨论结构体的值语义,我猜你指的是)。 - leppie
@leppie:结构类型的存储位置不过是该类型字段连接在一起的东西。无法写入驻留在只读存储位置中的结构体字段,也无法将只读结构体位置作为“ref”参数传递。由于结构类型上的实例成员将“this”作为“ref”参数,因此实际上无法在只读结构上调用实例成员。不幸的是,C#假定如果readOnlyStruct.method()无法将readOnlyStruct作为ref参数传递给method,... - supercat
编译器应该自动将代码重写为 var temp = readOnlyStruct; temp.method();,而不给结构类型任何发言权。这将确保 readonly 结构的任何字段都不能被写入,但是在不必要的情况下,额外的复制将减慢代码速度,并且最多只能交换一组不正确的语义,如果代码尝试写入它。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接