Должен ли аксессор 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, чтение переменных может занять больше времени, чем деление.
Используйте первый метод. Если это кажется медленным, то рассмотрим второе.