Должен ли аксессор C# использовать приватную переменную или вычислять на лету?

Какая практика программирования лучше и почему?

У меня есть такой класс:

class data {

    public double time { get; internal set; }
    public double count { get; internal set; }
    public average_count { ... }

}

Где Average_count должен быть read_only и дать расчет количества / времени.

Лучше написать аксессор как:

public average_count { get {

    return (time == 0) ? 0 : (count / time);

}}

Или я должен сделать что-то вроде:

private _avg_count;

public average_count {
    get 
    {
        return _avg_count;
    }

    internal set
    {
        return _avg_count;
    }
}

Где _avg_count обновляется, когда во времени и кол-ве устанавливаются аксессоры?

Кажется, что первое легче читать, но оно может быть медленнее, если часто обращаться к average_count. Сможет ли оптимизация компилятора сделать разницу незначительной?

8 ответов

Решение

Делая это на лету, мы получаем более читаемый код. Предварительный расчет может улучшить производительность, но вы должны делать это только в том случае, если (а) это необходимо и (б) вы профилировали, и это имеет значение.

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

Это такой, казалось бы, простой вопрос, но на него почти невозможно ответить. Причина в том, что то, что "правильно", зависит от ряда факторов.

1. Производительность

В своем ответе Скиллдрик рекомендует, чтобы вы предпочитали то, что читается, а не то, что работает лучше всего, как правило:

Надежность и ремонтопригодность [R] должны быть принесены в жертву ради производительности, только когда это абсолютно необходимо.

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

Так что это решение суда с вашей стороны. Лучший совет - вести профиль всякий раз, когда у вас есть подозрения; и если в вашем случае целесообразно жертвовать удобочитаемостью ради производительности, то вам следует это сделать.

2. Использование памяти

0xA3 предлагает довольно элегантный подход, обеспечивающий некоторый компромисс: только вычисляйте значение по мере необходимости, а затем кэшируйте его.

Недостатком этого подхода, конечно, является то, что для его обслуживания требуется больше памяти. int? требует по существу того же объема памяти, что и int плюс bool (что из-за проблем с выравниванием может означать 64 бита вместо 40). Если у вас есть множество экземпляров этого data класс и память - это дефицитный ресурс на платформе, на которую вы нацелены, увеличение числа типов до 32 бит на экземпляр может оказаться не самым умным ходом.

3. Ремонтопригодность

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

Суть в том, что только вы знаете свои точные обстоятельства и, следовательно, вы в лучшем положении, чтобы решить, что делать здесь.

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

class Foo
{
    int? _cachedResult = null;

    int _someProperty;
    public int SomeProperty
    {
        get { return _someProperty; }
        set { _someProperty = value; _cachedResult = null; }
    }

    int _someOtherProperty;
    public int SomeOtherProperty
    {
        get { return _someOtherProperty; }
        set { _someOtherProperty = value; _cachedResult = null; }
    }

    public int SomeDerivedProperty
    {
        get
        {
            if (_cachedResult == null)
                _cachedResult = someExpensiveCalculation();

            return (int)_cachedResult;
        }
    }
}

Зависит от того, как часто вы будете звонить average_count и как часто count а также time модифицированы. Для такой оптимизации я бы предложил использовать профилировщик.

Я бы пошел с вашим первым вариантом. Это объекты в памяти, поэтому вычисление того, что вы делаете, будет чрезвычайно быстрым. Кроме того, если вы создали для этого выделенное свойство (например, average_count), вам нужно будет добавить больше кода, чтобы пересчитать его в установщике как для времени, так и для количества.

В качестве дополнительного примечания (поскольку вы спрашиваете о передовой практике), вы должны использовать оболочку Pascal в C#, которая является начальными заглавными буквами и не подчеркивает.

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

Не оптимизируйте преждевременно.

Если вы заботитесь о безопасности потоков, возможно, будет проще сделать второй вариант, чем первый.

private double _avg_count;
static readonly object avgLock = new object();

public double time { get; internal set; }
public double count { get; internal set; }


public double average_count {
    get 
    {
        return _avg_count;
    }


}

private void setAverageCount()
{
    _avg_count = time == 0 ? 0 : (count / time);
}


 public void Increment()
 {
     lock (avgLock)
     {
         count += 1;
         setAverageCount();
     }


 }


 public void EndTime(double time)
 {
     lock (avgLock)
     {
         time = time;
         setAverageCount();

     }
 }

Сможет ли оптимизация компилятора сделать разницу незначительной?

Зависит от того, что вы считаете "значительным". Чтение varaible довольно быстро. Разделить два числа довольно быстро. Фактически, в зависимости от того, что находится в кеше RAM, чтение переменных может занять больше времени, чем деление.

Используйте первый метод. Если это кажется медленным, то рассмотрим второе.

Другие вопросы по тегам