Проблемы с добавлением ленивого ключевого слова в C # - PullRequest
38 голосов
/ 11 мая 2011

Я хотел бы написать код, подобный этому:

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. Считаете ли вы это полезным?

Я не начинаю крестовый поход, чтобы включить это в следующую версию библиотеки, но мне любопытно, какие соображения должна пройти такая функция, как эта.

Ответы [ 7 ]

56 голосов
/ 11 мая 2011

Мне любопытно, какие соображения должна пройти такая функция.

Прежде всего, я пишу блог на эту тему, среди прочего.Смотрите мой старый блог:

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

и мой новый блог:

http://ericlippert.com

, где можно найти множество статей по различным аспектам языкового дизайна.

Во-вторых, процесс разработки C # теперь открыт для всеобщего обозрения, поэтому вы можете сами убедиться в том, что учитывает команда разработчиков языка при проверке предложений новых функций.Подробнее см. https://github.com/dotnet/roslyn/.

Какие затраты будут связаны с добавлением этого вида ключевых слов в библиотеку?

Это зависит от многих вещей.Конечно, нет дешевых и простых функций.Есть только менее дорогие, менее сложные функции.Как правило, это затраты, связанные с разработкой, определением, внедрением, тестированием, документированием и обслуживанием этой функции.Есть и более экзотические затраты, такие как альтернативная стоимость неэффективной функции или стоимость выбора функции, которая плохо взаимодействует с будущими функциями, которые мы могли бы добавить.

В этом случае функция будетвероятно, просто сделайте ключевое слово "lazy" синтаксическим сахаром для использования Lazy<T>.Это довольно простая функция, не требующая особого синтаксического или семантического анализа.

В каких ситуациях это может быть проблематично?

Я могу вспомнить ряд факторов, которые заставили бы меня отодвинуть эту функцию.

Во-первых, это не обязательно;это просто удобный сахар.Это на самом деле не добавляет новой силы в язык.Выгоды, кажется, не стоят затрат.

Во-вторых, и что еще более важно, это закрепляет особую разновидность лени в языке.Существует более одного вида лени, и мы можем ошибиться.

Как может быть более одного вида лени?Хорошо, подумайте, как это будет реализовано.Свойства уже "ленивы" в том смысле, что их значения не рассчитываются до тех пор, пока не будет вызвано свойство, но вам нужно больше;Вы хотите свойство, которое вызывается один раз, а затем значение кэшируется в следующий раз.Под «ленивым», по сути, вы подразумеваете запомненную собственность.Какие гарантии нам нужно предоставить?Есть много возможностей:

Возможность # 1: Поток вообще не безопасенЕсли вы впервые вызываете свойство в двух разных потоках, может произойти все что угодно.Если вы хотите избежать условий гонки, вы должны добавить синхронизацию самостоятельно.

Возможность # 2: Потокобезопасен, так что два вызова свойства в двух разных потоках вызывают функцию инициализации, а затем пытаются выяснить, кто заполняет фактическое значение в кэше.Предположительно, функция будет возвращать одно и то же значение в обоих потоках, поэтому дополнительные затраты здесь заключаются просто в потраченном дополнительном вызове.Но кэш является поточно-ориентированным и не блокирует поток.(Поскольку потокобезопасный кэш может быть записан с использованием кода с низкой блокировкой или без блокировки.)

Код для обеспечения безопасности потоков стоит дорого, даже если это код с низкой блокировкой.Это приемлемая стоимость?Большинство людей пишут, что фактически являются однопоточными программами;Правильно ли добавлять накладные расходы на безопасность потоков в каждый вызов ленивого свойства, нужно ли это или нет?

Возможность # 3: Потокобезопасен, так что есть надежная гарантия, что функция инициализации будет вызываться только один раз;в кеше гонки нет.Пользователь может иметь неявное ожидание, что функция инициализации вызывается только один раз;это может быть очень дорого, и два вызова в двух разных потоках могут быть неприемлемы.Реализация такого рода лени требует полной синхронизации, когда возможно, что один поток блокируется на неопределенное время, пока ленивый метод выполняется в другом потоке.Это также означает, что могут быть взаимоблокировки, если есть проблема с упорядочиванием блокировок в ленивом методе.

Это добавляет еще больше затрат к этой функции, стоимость, которую в равной степени несут люди, которые не пользуются ею (потому что они пишут однопоточные программы).

Так как мы с этим справимся?Мы могли бы добавить три функции: «lazy not threadsafe», «lazy threadsafe with races» и «lazy threadsafe with blocking» и, возможно, взаимоблокировки ».И теперь эта функция стала намного дороже и труднее документировать.Это создает огромную проблему обучения пользователей .Каждый раз, когда вы предоставляете разработчику такой выбор, вы предоставляете ему возможность писать ужасные ошибки.

В-третьих, заявленная функциональность кажется слабой.Почему лень следует применять только к свойствам?Кажется, что это может быть применено, как правило, через систему типов:

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<T>, чтобы сделать работу за меня.Было бы небольшое удобство.

5 голосов
/ 11 мая 2011

В системной библиотеке уже есть класс, который делает то, что вы хотите: System.Lazy<T>

Я уверен, что он может быть интегрирован в язык, но как Эрик Липперт скажет вам, что добавление функций в язык не является чем-то легким.Многие вещи должны быть рассмотрены, и соотношение выгод / затрат должно быть очень хорошим.Поскольку System.Lazy уже достаточно хорошо справляется с этим, я сомневаюсь, что мы увидим это в ближайшее время.

3 голосов
/ 05 июля 2011

Если вы не возражаете против использования посткомпилятора, CciSharp имеет эту функцию :

class Zebra {
  [Lazy] public int StripeCount {
    get { return ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce(); }
  } 
} 
3 голосов
/ 11 мая 2011

Вы пробовали / Вы имеете в виду это?

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

РЕДАКТИРОВАТЬ:

после редактирования вашего поста, я бы добавил, что ваша идея определенно более элегантна, но все еще имеет ту же функциональность.!!!.

3 голосов
/ 11 мая 2011

Это вряд ли будет добавлено к языку C #, потому что вы можете легко сделать это самостоятельно, даже без Lazy<T>.

Простой , но не поточно-ориентированный , пример:

class Zebra
{
    private int? stripeCount;

    public int StripeCount
    {
        get
        {
            if (this.stripeCount == null)
            {
                this.stripeCount = ExpensiveCountingMethodThatReallyOnlyNeedsToBeRunOnce();
            }
            return this.stripeCount;
        }
    }
}
3 голосов
/ 11 мая 2011

Знаете ли вы о классе Lazy<T>, который был добавлен в .Net 4.0?

http://sankarsan.wordpress.com/2009/10/04/laziness-in-c-4-0-lazyt/

2 голосов
/ 11 мая 2011

Посмотрите на тип Lazy<T>.Также спросите Эрика Липперта о добавлении подобных вещей в язык, он, несомненно, будет иметь представление.

...