在C#中添加“lazy”关键字的问题

38

我很希望能够编写像这样的代码:

class Zebra
{
    public lazy int StripeCount
    {
        get { return ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce(); }
    }
}

编辑:为什么?我认为这比以下内容更好看:

class Zebra
{
    private Lazy<int> _StripeCount;

    public Zebra()
    {
        this._StripeCount = new Lazy(() => ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce());
    }

    public lazy int StripeCount
    {
        get { return this._StripeCount.Value; }
    }
}

第一次调用该属性时,它会运行get代码块中的代码,并在之后只返回其值。

我的问题:

  1. 添加这种关键字到库中需要什么成本?
  2. 在哪些情况下这可能会引起问题?
  3. 您觉得这有用吗?

我不是要发起一场把这个功能加入库的运动,但我很好奇这样一个特性需要经过哪些考虑才能实现。


2
语言设计者总是更喜欢通过库添加功能,而不是通过语言本身添加。 - David Heffernan
1
我正在考虑它如何与自动属性交互。 - R. Martinho Fernandes
@MartinhoFernandes:或者任何具有setter的属性 - Nick Larsen
8
@David: 不一定。我们多年来一直在图书馆中添加数据访问功能,包括ADO、DAO、ADO.NET、ODBC和其他大量的数据访问技术,但它们都没有提供一个一致的接口。这就是推动LINQ的原因;通过将排序、搜索等操作放入语言中,我们可以为许多不同的底层数据提供程序提供通用接口。将东西放入语言中非常棒,但它也很昂贵,因此我们希望将其保留给最有价值的功能。 - Eric Lippert
这并不值得回答,但添加关键字将会破坏一些现有的代码。我曾在C++潜在功能讨论中看到过这个论点,尽管我猜想在微软架构师心中它可能具有稍低的成本。Eric提供了另一个关键字是特殊的解释(通常保留给高影响力的功能),尽管我认为它是类似的。 - Merlyn Morgan-Graham
显示剩余2条评论
7个回答

56

我很好奇这样的功能需要经过哪些考虑。

首先,我写了一篇关于这个主题的博客,还有其他的。请看我的旧博客:

http://blogs.msdn.com/b/ericlippert/

和我的新博客:

http://ericlippert.com

有许多关于语言设计各个方面的文章。

其次,C#设计过程现在对公众开放,因此您可以自行查看语言设计团队在审核新功能建议时考虑的内容。详情请参见https://github.com/dotnet/roslyn/

添加此类关键字的成本是什么?

这取决于很多因素。当然,没有便宜、易实现的功能,只有成本较低、难度较小的功能。一般来说,成本包括设计、规定、实现、测试、文档编写和维护功能。还有更多奇特的成本,比如错过了实现更好功能的机会成本,或者选择与未来想要添加的功能相互作用不良的功能的成本。

在这种情况下,该功能可能只是将“lazy”关键字作为使用Lazy<T>的语法糖。这是一个非常直接的功能,不需要太多花哨的语法或语义分析。

这会在哪些情况下成为问题?

我可以想到许多因素会使我反对该功能。

首先,它并非必需;它只是一种方便的语法糖。它并没有真正为语言增加新的功能。好处似乎不值得成本。

其次,更重要的是,它将一种特定类型的惰性编码固定在语言中。有多种类型的惰性编码,我们可能会选择错误的类型。

有多种懒加载的方式,你需要一个被称为记忆化属性的属性。要实现它,我们需要什么保证呢?有很多可能性:1. 完全不支持多线程。如果在两个不同的线程上第一次调用该属性,那么任何事情都可能发生。如果想避免竞争条件,必须自己添加同步。2. 线程安全,这样在两个不同的线程上调用该属性时,它们都会调用初始化函数,然后竞争看谁填充缓存中的实际值。理论上,该函数将在两个线程上返回相同的值,因此额外的成本只是浪费了一次额外的调用。但是缓存是线程安全的,并且不会阻塞任何线程。(因为线程安全缓存可以使用低锁定或无锁代码编写)。实现线程安全的代码需要代价,即使是低锁定代码也是如此。这个代价是否可接受?大多数人编写的程序实际上是单线程的;是否应该将线程安全的开销添加到每个懒加载属性调用中,无论是否需要呢?
可能性3:线程安全,可以强有力地保证初始化函数只被调用一次;缓存没有竞争。用户可能会默认初始化函数只会被调用一次;这可能非常昂贵,两个不同线程上的两次调用可能是不可接受的。实现这种懒惰需要完全同步,其中一个线程在另一个线程上运行懒惰方法时可能会无限期地阻塞。这也意味着如果懒惰方法存在锁定顺序问题,则可能会出现死锁。

这增加了更多成本,这些成本由那些不利用它的人(因为他们编写单线程程序)平等承担。

那么我们该如何处理呢?我们可以添加三个特性:“不是线程安全的延迟”,“带有竞争的线程安全延迟”和“带有阻塞和可能死锁的线程安全延迟”。现在,该功能变得更加昂贵,而且难以记录。这会产生巨大的用户教育问题。每当您给开发人员提供这样的选择时,都会为他们提供编写糟糕错误的机会。

第三,该功能似乎很弱。为什么只将延迟应用于属性?似乎可以通过类型系统普遍应用:

lazy int x = M(); // doesn't call M()
lazy int y = x + x; // doesn't add x + x
int z = y * y; // now M() is called once and cached.
               // x + x is computed and cached
               // y * y is computed

我们试图避免小而弱的功能,如果有更一般的功能是它的自然扩展。但现在我们正在谈论真正严重的设计和实现成本。
“你觉得这个有用吗?”
“就我个人而言?并不是很有用。我写了很多简单的低锁定惰性代码,主要使用Interlocked.Exchange。(我不在乎懒惰方法被运行两次,其中一个结果被丢弃;我的惰性方法从来不那么昂贵。)模式很简单,我知道它是安全的,永远不会为委托或锁分配额外的对象,如果我有更复杂的东西,我总是可以使用Lazy来为我完成工作。这将是一个小便利。”

我在想,一个编译器可扩展性功能(可以扩展语法,如Nemerle)是否能够允许用户添加他们需要的确切语法糖,而无需询问语言设计者?这种元编程方式是否在C# 5的开发计划中?我猜这种问题将不再被提出... - Jordão
1
@Jordão:回答你的第一个问题:是的。回答你的第二个问题:不是。下一个版本C#的主要特性将是异步性。稍微扩展一下第一个问题:我们都认为元编程总体上很棒,希望让C#更能够进行元编程。然而,这并不一定意味着允许用户定义根本不同的语法是一个好主意。那可能会导致语言分裂成一堆互相无法理解的方言,我们希望避免这种结果。 - Eric Lippert
@Eric:我完全同意。当我第一次看到C# 1.0时,看到那些你放入元素(属性)中的奇怪注释时,我以为我可以使用它们来更改生成的代码。但很失望地得知它们只是编译器的“不透明”元数据。但无论如何,我认为它们是添加这些功能的第一道攻击线:编译器在编译期间调用的属性。就像CciSharp所做的那样。 - Jordão
它解决了OP提出的确切问题 - Jordão
此外,您在自己的项目中也会从中受益,例如在使用代码契约时。 - Jordão
@Jordão:哈哈,我正打算发表一条评论,说代码合同是一种在具有“智能”编译时属性处理的语言中真正有效的功能。随着我们继续开发“编译器作为服务”的设计,我们将继续思考这些编译时场景。 - Eric Lippert

5
系统库已经有一个可以实现你想要的功能的类:System.Lazy<T>
我相信它可以被集成到语言中,但正如Eric Lippert所说,向语言添加功能并不是轻而易举的事情。许多事情必须考虑,并且利益/成本比必须非常好。由于System.Lazy已经很好地处理了这个问题,我怀疑我们不会很快看到这种情况。

你如何计算添加关键字的收益成本比?例如async/await关键字?为什么这些关键字得到了特殊对待,而不是只返回适当的类? - Nick Larsen
@NickLarsen:使用await/async,背后状态机的实现是增加严重价值的地方。 - Jeff Yates
3
@NickLarsen说:"await"引发的程序转换非常巨大。它完全将方法重写为延续传递风格。虽然这是一项非常昂贵的功能,但是它具有巨大的优势。关键字方法将复杂的“内外颠倒”的代码转化为直接线性代码,并将将其重写为编译器的疯狂形式的负担放在编译器上。这对客户真正有益处。 - Eric Lippert

3

3

如果您不介意使用后编译器,CciSharp 就有这个功能

class Zebra {
  [Lazy] public int StripeCount {
    get { return ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce(); }
  } 
} 

3

这个功能不太可能被添加到C#语言中,因为即使没有使用Lazy<T>,你也可以很容易地自己实现。

以下是一个简单但非线程安全的示例:

class Zebra
{
    private int? stripeCount;

    public int StripeCount
    {
        get
        {
            if (this.stripeCount == null)
            {
                this.stripeCount = ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce();
            }
            return this.stripeCount;
        }
    }
}

1
你可以很容易地自己实现简单属性,但是自动属性被添加到语言中。这表明自己实现它的容易程度并不是关键所在。更多的是成本效益比(似乎总是如此)。 - R. Martinho Fernandes
自动实现属性对大多数C#/VB项目都有益处,但是为处理已经有简单解决方法的情况添加语言结构,并且对语言的表达能力增加相对较少,这种做法不太可能被高度优先考虑。 - Stephen Jennings
2
请注意,您在这里做出的假设是该代码仅从一个线程访问。如果从多个线程访问此代码,则存在竞态条件。 - Eric Lippert

3
你尝试过吗?/ 你是指这个吗?
private Lazy<int> MyExpensiveCountingValue = new Lazy<int>(new Func<int>(()=> ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce()));
        public int StripeCount
        {
            get
            {
                return MyExpensiveCountingValue.Value;
            }
        }

编辑:

在你编辑的帖子后,我想补充说,你的想法确实更加优雅,但仍然具有相同的功能!!!


2

看一下Lazy<T>类型。此外,询问Eric Lippert是否可以将此类内容添加到语言中,他无疑会有自己的看法。


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