Welcome! * Добро Пожаловать!
|
||||||||
Типичные ошибки программирования на C#или когда код выглядит верно, но не работает.Данная статья посвящена анализу наиболее часто встречающихся ошибок в C#-коде, и написана на основе двухлетнего опыта разработки ПО для обнаружения и анализа нефтяных пятен на поверхности воды. Все упомянутые в статье ошибки описаны в литературе; соответственно статья представляет собой нечто вроде проверочного списка или сборника типичных анти-паттернов, основанного на практическом опыте.
Согласно правилам преобразования типов C#, при делении целого числа на целое мы получаем в ответе целое число. Таким образом, прямое перенесение математических формул в текст программы даёт некорректные результаты.
Например, рассмотрим задачу, в которой необходимо вычислить точку пересечения прямой и оси Y, при том что прямая задана двумя точками. Несложно вывести формулу для Y-координаты: ,
double y=y0-x0/(x1-x0)*(y1-y0);
Ещё одна задача, приводящая к подобной ошибке - это задача о проекции диапазона. Допустим, у нас есть два диапазона, задаваемых целыми числами [A..B] и [C..D], и значение x1 из первого диапазона. Предполагая, что начало и конец диапазонов в некотором смысле соответствуют друг другу, найдём точку x2, которая соответствует точке x1 в этом же смысле: . При прямой подстановке формулы в программу мы для всех точек из диапазона [A..B] в качестве ответа получим точку С, т.к. второе слагаемое при целочисленных переменных - всегда 0.
Единицы, в которых - как Вы предполагаете! - измеряется некая величина, не имею ничего общего с теми значениями, которые там в действительности хранятся. Проблема может возникать вне зависимости от Вашей аккуратности, т.к. данные могут приходит из внешнего источника (файл, база данных, измерительный прибор), при этом документация к форматам данных может отсутствовать или быть неполной/некорректной.
Для примера рассмотрим следующую строчку (легко заметить, что правила хорошего стиля нарушены - это сделано нарочно для большей наглядности): double widthInMeters = height * Math.Tan(alpha);
Здесь мы неявно предполагаем, что высота у нас измеряется в метрах, а угол - в радианах. Если хоть одно из этих предположений нарушено (например, кто-то передаёт высоту в пикселях, а углы - в градусах) - у нас проблемы!
Эта проблема более относится к стилю программирования, нежели к проблемам компилятора; поэтому и простые решения здесь отсутствуют. Тем не менее, кое-что сделать всё-таки можно.
По названию функции невозможно определить, что является её результатом, а что - параметром. Проблема ещё более усугубляется, если определены две взаимодополняющие функции с невнятными названиями.
Пример "по мотивам" реального кода: class DataObject
{
int GetVerticalAngleRowNumber(int value)
{
//do something, using exemplar values
return result;
}
int GetRowNumberVerticalAngle(int value)
{
//do something, using exemplar values
return result;
}
}
int number = dataObject.GetVerticalAngleRowNumber(alpha);
int angle = dataObject.GetRowNumberVerticalAngle(number);
Вышеприведенные строчки выглядят абсолютно нормально; даже имена функций похожи на хорошие - Camel Case, английские глаголы и существительные… К сожалению, как выяснилось, порядок слов не стопроцентно определял смысл - так что тот, кто писал функции и тот, кто их вызывал, использовали диаметрально противоположные трактовки (так, первая функция на самом деле принимала номер ряда и возвращала угол).
Называйте функции настолько внятно, чтобы не было возможности понять Вас неправильно. Так, функции из примера были в итоге переименованы в GetRowNumberByVerticalAngle и GetVerticalAngleByRowNumber, что устранило путаницу. Вот как стал выглядеть код: class DataObject
{
int GetVerticalAngleByRowNumber(int
Также следует заметить, что использование доменных типов данных (и вообще разных типов для номеров и углов) эффективно выявило бы ошибку на этапе компиляции.
Одновременный доступ к одной переменной из разных потоков приводит к непредсказуемому результату.
Рассмотрим следующий код: if (instance != null)
{
//do whatever
//...
instance.member = 1;
}
Где вместо Коварство Race Conditions состоит в том, что ошибка проявляется не всегда - она может проявляться только в определённое время или на определённом компьютере. Вместо NullReference Вам может встретиться ArrayOutOfBounds, если в качестве предусловия проверяется длина массива, и т.п.
«Каждый, кто собирается работать с потоками, должен чётко знать, что он делает» - звучит хорошо, но не всегда выполнимо, также как и совет писать только потокобезопасные функции. Тем не менее, к этому стоит стремиться - но если не можешь избежать потоконебезопасности, весь код, содержащий побочные эффекты, следует помещать внутрь критической секции.
Race Conditions - достаточно хорошо описанный в литературе антипаттерн.
Несколько раз обращаясь к элементу массива (особенно многомерного, и/или со сложной адресацией) в пределах нескольких строчек, очень легко не заметить, что одно из обращений набрано с ошибкой.
Пример написан по мотивам реального кода: //Проходим по квадратному массиву данных и отрисовываем его на экране.
//Следует помнить, что ось Y на экране направлена вниз,
//так что индекс y отсчитывается с конца массива
for(int x=0; x<length; x++)
{
for(int y=0; y<length; y++)
{
if (data[length - y - 1, x] < value1)
{
color = color1;
}
else
if (data[length - y - 1, x] > value2)
{
color = color2;
}
else
{
color = color1 + (int)((double)data[length - x - 1, y] - value1)/(value2 - value1) * (color2 - color1);
}
SetPixelOnScreen(x, y, color);
}
}
Если не присматриваться специально, очень просто не заметить, в чём фокус - в последнем случае при обращении к элементу массива перепутаны индексы. Естественно, этот код не всегда даст правильный результат.
Решение очень простое - достаточно отрефакторить код, введя переменную для анализируемого элемента массива. Поскольку присваивание переменной осуществляется только однажды - глаз "не замыливается" и проверить правильность очень легко. Вот как выглядит код после исправления: //Проходим по квадратному массиву данных и отрисовываем его на экране.
//Следует помнить, что ось Y на экране направлена вниз,
//так что индекс y отсчитывается с конца массива
for(int x=0; x<length; x++)
{
for(int y=0; y<length; y++)
{
int element = data[length - y - 1, x];
if (element < value1)
{
color = color1;
}
else
if (element > value2)
{
color = color2;
}
else
{
color = color1 + (int)((double)element- value1)/(value2 - value1) * (color2 - color1);
}
SetPixelOnScreen(x, y, color);
}
}
Д.Булдаков предложил более эффективный и "читабельный" вариант: //Проходим по квадратному массиву данных и отрисовываем его на экране.
//Следует помнить, что ось Y на экране направлена вниз,
//так что индекс y отсчитывается с конца массива
double fitting = 1/(value2 - value1) * (color2 - color1);
for(int x=0; x<length; x++)
{
for(int y=0, y_data = length-1; y<length; y++, y_data--)
{
int element = data[y_data, x];
if (element < value1)
{
color = color1;
}
else
if (element > value2)
{
color = color2;
}
else
{
color = color1 + (int)((element - value1)*fitting);
}
SetPixelOnScreen(x, y, color);
}
}
Родственным антипаттерном в данном случае следует признать антипаттерн "Copy-Paste", хотя происхождение данного антипаттерна (по аналогии можно назвать его "Copy-Type") прямо противоположное - перенабрать десяток символов проще, чем скопировать; но и ошибку при перенаборе допустить просто. Тем не менее, разница между антипаттернами "Copy-Type" и "Copy-Paste" только в происхождении проблемного участка кода; портят жизнь программисту они абсолютно одинаково. Скачать текст статьи в формате RTF. В архив также включена англоязычная презентация. |
||||||||
E-mail: yuriy@silvestrov.com * ICQ 56089061 * Phone: (+380 572)64-96-09 |