代码之家  ›  专栏  ›  技术社区  ›  Eric Burnett

考虑C中的“一次性”关键字#

  •  13
  • Eric Burnett  · 技术社区  · 5 年前

    你对如何在.NET中实现一次性对象有什么看法?如何解决实现IDisposable类的重复性?

    我觉得可识别的类型并不是他们应该拥有的第一流公民。太多的问题由开发人员决定。

    具体地说,我想知道在语言和工具方面是否应该有更好的支持,以确保一次性的东西得到正确的实现和正确的处理。

    例如,在C中,如果需要实现可释放语义的类可以这样声明:

    public class disposable MyDisposableThing
    {
        ~MyDisposableThing()
        {
            // Dispose managed resources
        }
    }
    

    在这种情况下,编译器可以轻松生成IDisposable接口的实现。析构函数~MyDisposableThing可以转换为应释放托管资源的实际Dispose方法。

    中间C代码如下:

    public class MyDisposableThing : IDisposable
    {
        private void MyDisposableThingDestructor()
        {
            // Dispose my managed resources
        }
    
        ~MyDisposableThing()
        {
            DisposeMe(false);
        }
    
        public void Dispose()
        {
            DisposeMe(true);
            GC.SuppressFinalize(this);
        }
    
        private bool _disposed;
        private void DisposeMe(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // Call the userdefined "destructor" 
                    MyDisposableThingDestructor();
                }
            }
            _disposed = true;
        }
    }
    

    这将使代码更干净,处理代码的样板文件更少,并且可以使用一致的方法来处理托管资源。对于边缘案例和非托管资源,仍然支持手工实现IDisposable。

    确保正确地处理实例是另一个挑战。请考虑以下代码:

    private string ReadFile(string filename)
    {
        var reader = new StreamReader();
        return reader.ReadToEnd(filename);
    }
    

    reader变量永远不会超出方法的范围,但必须等待gc处理它。在这种情况下,编译器可能会引发一个没有显式释放streamreader对象的错误。此错误将提示开发人员将其包装为using语句:

    private string ReadFile(string filename)
    {
        using (var reader = new StreamReader())
        {
            return reader.ReadToEnd(filename);
        }
    }
    
    7 回复  |  直到 5 年前
        1
  •  24
  •   Eric Lippert    15 年前

    一个常见的原则是“需要设计模式来解决语言缺陷”。这就是这个原理的一个例子。我们需要一次性的模式,因为语言不能提供给你。

    我同意可处置性 能够 已经从“模式”世界提升到C语言领域,正如我们对属性getter和setter(这是getter和setter方法模式的标准化)或事件(它标准化了存储委托并在发生有趣的事情时调用它的想法)所做的那样。

    但是语言设计是昂贵的,并且有有限的工作量可以应用于它。因此,我们试图找到最有用的、最有说服力的模式,使之适合语言。我们试图找到一种不仅方便而且能为语言增加更多表达力的方法。例如,Linq将过滤、投影、连接、分组和排序数据的概念移动到语言中,这为语言增加了很多表达能力。

    虽然这确实是个好主意,但我认为它不符合标准。我同意,这将是一个很好的便利,但它不支持任何真正丰富的新场景。

        2
  •  6
  •   Greg Beech    15 年前

    除了其他答案之外,还有一个问题,即这个工具应该实现多少,人们应该从中期望什么?假设我像这样宣布我的班级:

    public disposable class MyClass
    {
        readonly AnotherDisposableObject resource = new AnotherDisposableObject();
    
        ~MyClass()
        {
            this.resource.Dispose();
        }
    
        public void DoStuff()
        {
            this.resource.SomeMethod();
        }
    }
    

    如果有人打电话给你,你会怎么想? DoStuff 之后 实例已被释放?编译器是否应该自动插入类似

    if (this.disposed) throw new ObjectDisposedException();
    

    在每个方法的开头,因为您已经声明类是可释放的?

    如果是这样,那么在释放对象后显式允许调用方法的情况如何(例如 MemoryStream.GetBuffer )?您是否需要引入一个新的关键字来向编译器指出这一点,例如 public useafterdispose void ... ?

    如果没有,那么你如何向人们解释这个新关键字为你实现了一些样板代码,但是他们仍然需要编写代码来检查对象是否在每个方法中被释放?此外,如何 可以 他们甚至检查这个,因为关于对象是否被释放的所有状态信息都是自动生成的!现在他们需要在 ~MyClass 方法,它撤消编译器应该为您做的一半工作。

    我认为作为一种特定的语言模式,这个概念中有太多的漏洞,它只试图解决一个特定的问题。现在什么 能够 以通用方式解决这一类问题的方法是mixin(即一次性mixin),并且该语言特性通常可用于不同的概念(如等效mixin、可比较mixin等)。那是我的钱的去处。

        3
  •  5
  •   Noldorin    15 年前

    我个人认为 IDisposable 在当前的.NET版本中相当体面。存在 using 关键字使它成为我的一流构造。

    我承认有一定数量的样板代码,但不足以保证新的语言特性。(自动实现的属性是一个很好的例子,它是一个请求引入的特性。)您在文章中遗漏了一个重要的点,即这个“样板”代码不是 总是 你需要什么。主要是你需要处理 非受管的 外部资源 if (disposing) 块。

    当然,毁灭者( ~MyDisposableThing )无参数 Dispose() 方法是真正的样板文件,可以由语言关键字的用户删除,正如您所建议的那样-但我再次不确定引入一个实际的新关键字对于几行代码来说是完全必要的。

    我当然明白你在这里提出的观点,并在某种程度上对此表示同情。(我敢肯定,如果您的建议是语言规范的一部分,编码人员不会抱怨。)但是,当代码行数量相当有限时,它不太可能说服.NET开发团队,其中一些代码可以说是相当具体的上下文(因此不是样板文件)。

        4
  •  4
  •   Daniel Earwicker    15 年前

    我完全同意 IDisposable 需要更好的语言支持。这里是 my variant of it from a while ago .细节可能是错误的,但是C++/CLI是一个很好的模型。不幸的是,当我在C++和CLI中展示他们的例子时,它混淆了C程序员。但在实现方面,它已经做了“正确的事情”;我们只需要在C中使用一种新的语法。

    即使最简单的Windows窗体应用程序也具有 Dispose 其中的方法,由向导生成,在面对不精确的更改时很脆弱。把组件组合在一起,这样一个组件就可以“拥有”其他几个组件,这是非常基本的,以至于 不可分的 这几乎是不可避免的,不幸的是,似乎需要几页书来解释如何正确地实现它。

    现有的 using 语句负责客户端。我们需要更多语言支持的地方是在实现方面。

    类的某些字段引用了类“拥有”的内容,而有些字段则不属于类。因此,我们必须能够将一个字段标记为拥有。

    此外,自动生成终结器也是一个非常糟糕的主意。通常,类将拥有实现 不可分的 .不是所有的类都是线程安全的,它们也不应该是线程安全的。如果它们是从终结器调用的,则会在另一个线程上发生,从而强制它们是线程安全的。这可能是附近的一个地方 不可分的 这导致了最混乱的情况——很多人读了这些书,却误以为你 在支持的对象上编写终结器 不可分的 .

        5
  •  2
  •   Andrew Finnell    13 年前

    我意识到这是一条旧线,但有些东西被忽视了。

    C和Java中的模式都是没有确定性析构函数的根本原因。

    MyObject A = new MyObject()
    MyObject B = A;
    A.Dispose();
    

    现在B的状态是什么?如果B的所有者真的不想处理它呢?你现在在C++中有同样的问题,你必须跟踪你所持有的对象的所有引用。

    IDisposable 只有在 using() 并确保在发生异常时清理资源。

    有设计模式可以做到这一点,但事实并非如此 不可分的

    @彼得 是的,我认为可浸式模式有缺陷。在实现时,可释放模式通常并不意味着只处理操作系统资源,其思想是能够继续使用已处理的对象。通过在Ext{{}}最后使用{ }在Java中或在.NET中使用()来破坏GC的一个原因。我不是说记忆会泄露。我是说,现在可以让代码的其他部分引用已释放的对象。现在,开发人员有责任在每次调用之前检查对象是否已被释放,或者至少捕获一个objectDisposedException。

    让我们来看一个愚蠢的例子:

    FileStream stream = new FileStream (@"c:\mylog.txt");
    logger.setStream (stream);
    

    谁应该调用.dispose()?日志程序现在获取文件流的所有权可能并不明显。假设流是在开发人员之外的某个地方创建的,并且知道它将被设置为日志流。

    如果我们加一行,我们会破坏记录器

    using (FileStream stream = new FileStream (@"c:\mylog.txt"))
        { logger.setStream (stream); }
    

    FileStream stream = new FileStream (@"c:\mylog.txt");
    logger.setStream (stream);
    stream.Dispose();
    

    可释放模式不引用资源计数。开发人员现在必须意识到谁拥有这个对象,谁负责清理它。真正的问题是,当调用Dispose()时,正常的行为是使整个对象失效,从而阻止使用它。

        6
  •  0
  •   supercat    14 年前

    imho,.NET语言在处理IDisposable时有一个主要缺点,那就是没有好的方法来处理抛出异常的初始值设定项。除非有人“泄漏”了正在构造的对象或其中的可释放对象的副本,否则无法清除在初始值设定项引发之前创建的任何IDisposable对象(在初始值设定项或基本级构造函数中)。

    为此,我想看到两个特性:

    1. 如果异常从其构造函数中抛出,将导致调用特定方法的类声明。
    2. 一种字段声明,指示该字段应具有.Dispose。如果在对象上使用了某些特殊的私有方法或关键字,则对该字段调用。

    顺便说一句,我还想看到一个结构方法的声明,它将表明该方法改变了底层结构。禁止在结构值上使用此类方法,在结构属性上使用此类方法将生成一个读-修改-写序列。

        7
  •  -1
  •   Chris    15 年前

    好的,您需要了解托管内存和非托管内存之间的区别。 简单地说,C++样式析构函数在C**的托管世界中是不起作用的,因为当对象被垃圾收集时不能保证,因此您永远不会知道析构函数何时被调用,这会使事情变得不可预测。

    与C++相反,当析构函数在类超出范围时被调用,这样当它被调用时就可以保证。

    这就是为什么C不能有析构函数的原因。