【问题标题】:Problems with adding a `lazy` keyword to C#向 C# 添加 `lazy` 关键字的问题
【发布时间】:2011-08-23 09:14:05
【问题描述】:

我很想写这样的代码:

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. 您觉得这有用吗?

我并没有开始努力将其纳入库的下一个版本,但我很好奇这样的功能应该经过什么样的考虑。

【问题讨论】:

  • 框架已经提供了Lazy<T>。关键字值得吗?
  • 语言设计师总是更喜欢将功能添加到库中而不是通过语言。
  • @JeffYates:这就是我要问的
  • 我正在考虑它会/应该/可以如何与自动属性交互。
  • @David:不一定。多年来,我们一直在尝试在库中添加数据访问功能,并获得了 ADO、DAO、ADO.NET、ODBC 以及其他数据访问技术的完整字母组合,其中没有一个提供一致的接口。这就是激励 LINQ 的原因。通过在语言中进行排序、搜索等,我们为许多不同的底层数据提供者提供了一个通用接口。用语言编写东西很棒,但代价高昂,因此我们希望只为最有价值的功能保留它。

标签: c# properties keyword


【解决方案1】:

【讨论】:

    【解决方案2】:

    看看Lazy&lt;T&gt; 类型。还请 Eric Lippert 在语言中添加这样的内容,他无疑会有意见。

    【讨论】:

      【解决方案3】:

      系统库已经有一个类可以做你想做的事:System.Lazy&lt;T&gt;

      我确信它可以集成到语言中,但正如Eric Lippert 会告诉您的那样,向语言中添加功能并不是一件容易的事。很多事情要考虑,收益/成本比需要非常好。由于System.Lazy 已经很好地处理了这个问题,我怀疑我们很快就会看到这个。

      【讨论】:

      • 如何计算添加关键字的收益/成本比?喜欢 async/await 关键字?为什么那些得到关键字的特殊处理而不是仅仅返回适当的类?
      • @NickLarsen:使用 await/async,幕后状态机的实现是非常有价值的地方。
      • @NickLarsen:因为“await”在程序上引起的转换是巨大的。它将方法完全重写为继续传递样式。这是一个非常昂贵的功能,但它有很大的好处。关键字方法将复杂的“由内而外”的代码变成了直接的线性代码,并将重写它的负担放到了编译器的疯狂形式上。这对客户来说是真正的好处。
      【解决方案4】:

      这不太可能被添加到 C# 语言中,因为即使没有Lazy&lt;T&gt;,您也可以自己轻松完成。

      一个简单的,但不是线程安全的,例子:

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

      【讨论】:

      • 您可以轻松地自己做简单的属性,但自动属性被添加到语言中。这表明,自己实现它的难易程度并不是问题的症结所在。更多的是成本效益比(似乎一直如此)。
      • 自动实现的属性对大多数 C#/VB 项目(如果不是全部的话)都是有益的,但是添加一个语言结构来处理已经具有手动和通过库的简单解决方法的案例,并且增加了相对较少的表达能力该语言不太可能被高度优先考虑。
      • 正是我想说的。我认为您应该将回答的重点从“因为它很简单”转移到您在那里所说的内容上。
      • 请注意,您在这里假设只能从一个线程访问。如果从多个线程访问此代码,则会出现竞争条件。
      【解决方案5】:

      你试过了吗/你是说这个吗?

      private Lazy<int> MyExpensiveCountingValue = new Lazy<int>(new Func<int>(()=> ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce()));
              public int StripeCount
              {
                  get
                  {
                      return MyExpensiveCountingValue.Value;
                  }
              }
      

      编辑:

      在您的帖子编辑后,我要补充一点,您的想法肯定更优雅,但仍然具有相同的功能!!!。

      【讨论】:

        【解决方案6】:

        我很好奇这样的功能应该经过什么样的考虑。

        首先,我写了一篇关于这个主题的博客。查看我的旧博客:

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

        还有我的新博客:

        http://ericlippert.com

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

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

        将这种关键字添加到库中会涉及哪些成本?

        这取决于很多事情。当然,没有便宜、简单的功能。只有更便宜、更简单的功能。一般来说,成本是涉及设计、指定、实施、测试、记录和维护功能的成本。还有更多奇特的成本,例如不做更好的功能的机会成本,或者选择与我们可能想要添加的未来功能交互不佳的功能的成本。

        在这种情况下,该功能可能只是让“lazy”关键字成为使用Lazy&lt;T&gt; 的语法糖。这是一个非常简单的功能,不需要大量花哨的句法或语义分析。

        这在什么情况下会出现问题?

        我能想到许多因素会导致我拒绝使用该功能。

        首先,没有必要;它只是一种方便的糖。它并没有真正为语言增加新的力量。收益似乎不值得付出代价。

        其次,更重要的是,它将一种特殊的惰性融入到语言中。懒惰不止一种,我们可能会选错。

        怎么会有不止一种懒惰呢?好吧,考虑一下如何实施。属性已经是“惰性的”,因为在调用属性之前不会计算它们的值,但您想要的还不止这些;您想要一个调用一次的属性,然后将值缓存以备下次使用。 “懒惰”本质上是指记忆属性。我们需要提供哪些保证?有很多可能性:

        可能性#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&lt;T&gt; 为我完成工作。这将是一个小小的便利。

        【讨论】:

        • 只是想知道,一种编译器可扩展性功能(它也可以扩展语法,就像在 Nemerle 中一样)是否允许用户添加他们需要的确切糖,而无需询问语言设计者?这种元编程在 C# 5 的管道上吗?我想这种问题不会再被问到了……
        • @Jordão:回答你的第一个问题:是的。回答你的第二个问题:没有。下一个 C# 版本的主要特性将是异步。稍微扩展第一个问题:我们都认为元编程总体上很棒,并且希望使 C# 更能够进行元编程。然而,这并不一定意味着允许用户定义全新的语法是一个好主意。这可能会导致语言分裂成一堆相互难以辨认的方言,我们希望避免这种结果。
        • @Eric:我完全同意。当我第一次看到 C# 1.0 并看到你放入元素(属性)中的那些奇怪的注释时,我认为我可以使用它们来更改生成的代码。得知它们对编译器来说“只是”不透明的元数据,这非常令人失望。但无论如何,我认为它们是添加这些功能的第一道防线:编译器在编译期间回调的属性。就像CciSharp 所做的那样。
        • ...它解决了OP询问的exact problem
        • 此外,您会在自己的项目中受益于此,例如代码合同。
        【解决方案7】:

        如果您不介意使用后编译器,CciSharpthis feature

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-10-12
          • 2019-02-25
          • 2013-06-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多