Большинство программистов учатся методом проб и ошибок. Это часть пути от Junior C# Developer до Senior C# Developer. Тем не менее, не обязательно совершать самому все эти ошибки, чтобы пройти этот путь.
Ниже приведены типичные ошибки, с которыми можно встретиться программируя на C#, и пути их обхода.
1. Итерация значений вместо использования LINQ
Очень часто при написании приложения стоит задача считать множество значений и сохранить их как список (List) или другую коллекцию. Сделать это можно, например, простой итерацией.
Рассмотрим создание списка клиентов. Если клиентов сотня тысяч, создать специфический набор данных, перебирая все эти записи — не совсем эффективно. Вместо использования операторов for или foreach можно использовать LINQ (Language-Integrated Query). Данный язык запросов, синтаксис которого напоминает SQL, фирма Microsoft добавила в языки программирования платформы .NET Framework в конце 2007 г. LINQ изначально был спроектирован для того, чтобы облегчить работу с такими объектами как коллекции.
Приведем пример:
Неэффективный способ:
foreach (Customer customer in CustomerList) {
if (customer.State == «FL») {
tax += customer.Balance;
}
}
Эффективный способ:
var customers = (from customer in CustomerList
where customer.State == "FL"
select customer.Balance).Sum();
Одна строчка кода на LINQ сразу возвращает набор данных, состоящий из 1000 клиентов, вместо перебора 100 000 объектов. Работать одновременно с 1000 значений более эффективно, чем перебирать 100 000.
2. Нелогичное использование «var», если известен тип данных
С момента появления ASP.NET MVC многие программисты стали использовать LINQ для работы с коллекциями. В большинстве случаев тип получаемых данных неизвестен. В этом случае оператор «var» помогает избежать ошибок при выполнении кода, в случае, если результатом является NULL или неожиданный тип данных.
Тем не менее, желательно явно декларировать тип данных, если он известен. Это улучшает читаемость кода, и помогает другим программистам работать с ним без лишних усилий.
Рассмотрим предыдущий пример:
var customers = (from customer in CustomerList
where customer.State == "FL"
select customer.Balance).Sum();
В этом примере, возможно, Sum должен иметь тип decimal. Если это точно известно, нужно задекларировать переменную как decimal. Когда другой программист будет читать этот код, он будет знать, что значение будет иметь тип decimal, а не int, например.
3. Использование глобальных переменных класса вместо свойств
Свойства являются обычными для объектно-ориентированных языков программирования. Они предоставляют гибкий механизм для чтения, записи или вычисления значения частного поля. Но зачем их использовать, если любые переменные класса можно объявить глобальными? Ответ один — свойства дают возможность контролировать как они будут использоваться, чего нельзя отнести к глобальным переменным.
Рассмотрим следующий код:
public decimal Total {get; protected set;}
В данном случае только сам класс или производные от него классы смогут его использовать. Рассмотрим класс Order, который подсчитывает общее количество заказов клиента. Не хотелось бы, чтобы какой-то внешний класс изменял что-то в заказах, а вот класс Order и производные от него могли бы совершать операции с переменными типа Total. Если просто сделать переменную глобальной, любой класс сможет сделать с ней все, что угодно, без каких-либо ограничений.
4. Забыть освободить объект
Нехватка памяти и других ресурсов компьютера — реальная проблема для различных приложений. C# предоставляет довольно удобный путь использовать метод Dispose, когда работа с объектом закончена. Для этого даже не обязательно этот метод указывать явно. Об этом позаботиться оператор «using».
Рассмотрим:
file.Read(buffer, 0, 100);
Вышеописанный код, если явно не указать закрытие объекта, может стать причиной нехватки оперативной памяти. Это легко предотвратить используя оператор «using».
using (FileStream file = File.OpenRead("numbers.txt")) {
file.Read(buffer, 0, 100);
}
Теперь приложение считает информацию из файла и закроет объект по окончании.
5. Использование «» вместо string.Empty
Это всего лишь небольшая досада для программиста, она больше влияет на читаемость кода, нежели на его эффективность. Например, «» может быть ошибочно понят как символ пробела « », а это уже совершенно другое значение. Используя string.Empty вместо «» для инициализации строковой переменной, можно избежать любую ошибочную двусмысленность.
6. Использование общих исключений Try – Catch
Многие начинающие программисты используют общий класс Exception вместо того, чтобы явно указывать тип перехваченного исключения. Конечно, все существующие в C# классы исключений являются производными от общего класса Exception и можно создать собственный класс исключений, который будет наследовать общий класс. Но в любом случае предпочтительно использование конкретных исключений вместо общих.
Неэффективный способ:
try {
newInteger = int.Parse(astring);
} catch (Exception e) {
// do something here
}
Эффективный способ:
try {
newInteger = int.Parse(astring);
} catch (FormatException) {
// do something here
}
Второй вариант кода уже явно указывает тип перехваченного исключения. Таким образом будет проще понять, что вызвало ошибку, и исправить ее. Конечно, можно использовать общий класс Exception для неизвестных возможных ошибок, но делать это надо, по возможности, реже.
7. Использование методов внутри блока Try – Catch
Ме?тоды являются основой объектно-ориентированного программирования. Выше был приведен пример простого блока Try — Catch, который содержит только один оператор с обработчиком исключения.
Обычная ошибка, которую совершают разработчики, — окружить метод блоком исключения Try – Catch .
Блоки Try – Catch лучше располагать в логической секции кода.
Правильно закодированный метод, должен сам выбрасывать исключение, чтобы передать его на верхний уровень для обработки.
Кроме того, не нужно использовать только один блок, если метод совершает несколько логически разных процедур, например, читает файл, распределяет его содержимое по переменным и затем записывает результат в базу данных. Разбейте блоки так, чтобы были видны: чтение файла, занесение данных, сохранение информации в базу данных.
8. Некорректное соединение строковых переменных
В старых версиях языка для соединения двух строковых переменных достаточно было использовать знак «+». Однако реализация данного пути не всегда была эффективной и Microsoft представил класс StringBuilder, созданный для оптимизации скорости и памяти при работе со строковыми величинами.
Используйте StringBuilder всякий раз, когда нужно соединить строки и для других операций над строковыми переменными. Конечно, для работы с совсем простыми строчками это делать не совсем обязательно, но для обработки данных полученных из файла или базы данных класс StringBuilder незаменим.
9. Не забудьте про журнал ошибок
Что произойдет, если вам позвонит пользователь и скажет, что приложение выдало ошибку? Как узнать, что произошло и что вызвало ошибку? Именно для этого и существуют журналы ошибок. Все ошибки всегда должны сохраняться в журнале.
Для этого можно использовать собственный код или настроенный под себя сторонний инструмент. Поскольку сторонних инструментов для создания журналов ошибок существует великое множество, возможно разумнее будет использовать уже существующий и проверенный, чем создавать свой собственный.
10. Наконец, не забудьте обновить свой код
Это ошибка не только программистов на C#, но на других языках.
Visual Studio объединена с Team Foundation Server (TFS). Это хорошая среда для команды разработчиков, но это не будет правильно работать, если программист забудет скачать утром обновленный код, перед тем как продолжить свою работу.
Несомненно, можно объединить 2 различных версии кода потом, но это не всегда делается корректно и может привести к неожиданным ошибкам.
Не нужно забывать скачивать последнюю версию кода перед началом работы и загружать на сервер ваши последние наработки в конце. Никогда не загружайте код с ошибками в нем. Только тот, который может быть скомпилирован. TFS имеет систему, позволяющую компилировать код «на лету». Безошибочное компилирование позволяет быть уверенным, что другие программисты могут скачать этот код и использовать его.
Заключение
C# — достаточно сложный язык. Когда начинаешь изучать объектно-ориентированное программирование, сталкиваешься с большим количеством трудностей. Здесь так много различных правил и стандартов, что непросто избежать ошибок. Однако, делая выводы из своих и чужих ошибок, можно стать хорошим программистом, который создает эффективные и продуктивные приложения.
Комментарии (33)
IL_Agent
25.05.2017 21:26+61. С linq код в данном случае декларативнее, но никак не эффективнее.
2. C# — язык со статичекской типизацией. В нем всегда известен тип выражения. Причем тут null вообще?
Сосмнительная статья. Вряд ли поможет пройти путь от джуна к лиду.
kekekeks
26.05.2017 15:53На самом деле зависит. Если это запрос к базе, то аггрегации лучше на ней же и выполнять, а не гонять по сети 10000 записей.
Lofer
26.05.2017 17:09-1Это несколько другой механизм: LINQ to SQL. И его работа зависит только от провайдер БД
mayorovp
26.05.2017 17:13+1LINQ to SQL — это ORM-библиотека, а не механизм.
Lofer
26.05.2017 17:30-2Вспомним мат-часть? :)
LINQ introduces standard, easily-learned patterns for querying and updating data, and the technology can be extended to support potentially any kind of data store. Visual Studio includes LINQ provider assemblies that enable the use of LINQ with .NET Framework collections, SQL Server databases, ADO.NET Datasets, and XML documents.
In LINQ to SQL, the data model of a relational database is mapped to an object model expressed in the programming language of the developer. When the application runs, LINQ to SQL translates into SQL the language-integrated queries in the object model and sends them to the database for execution. When the database returns the results, LINQ to SQL translates them back to objects that you can work with in your own programming language.
kekekeks
26.05.2017 17:53Кто-то свои LINQ-провайдеры ни разу не писал, я смотрю.
Lofer
26.05.2017 18:24Не вижу смысла спорить с терминологией «производителя», или его официальными примерами Walkthrough: Creating an IQueryable LINQ Provider
Или подразумевалось что-то другое?
MonkAlex
25.05.2017 22:19+4В статье куча подозрительных описаний, даже разбирать лень.
Кто составлял список? Всмысле, такое ощущение, что человек на шарпе писал лет 5 назад и что-то по памяти набросал.
x893
25.05.2017 22:33Мне кажется это перевод какой то древней статьи из msdn.
Сильно общё, спорно и куце (хотя даже если бесспорно, то порно остается).
wowaaa
25.05.2017 22:40+4По пунктам исходя из названия статьи:
1. Не ошибка — только если вся команда решила, что кодстайл будет только LINQ
2. Не ошибка — также кодстайл, и никто не мешает подвести курсор в студии к var, чтобы посмотреть текущий тип, если вы не уверены, что же используется. Вот если это в дальнейшем приводит к тому, что, к примеру, в var получился целочисленный тип, а мы дальше теряем знаки на делении, то это уже совсем другая ошибка.
3. Ошибка, но она слабо относится конкретно к C# — более того, это банальные принципы инкапсуляции в объектно ориентированном языке, а в C# еще и синтаксический сахар, т. к. в Java, к примеру, вам придется добавлять пару методов.
4. Ошибка, но опять же — это дополнительный синтаксический сахар, и если разработчик адекватно использует Dispose/Close методы, то ничто никуда не утечет.
5. Не ошибка, как автор сам себе и написал, но при этом забывает добавить о простейших методах IsNullOrEmpty и IsNullOrWhiteSpace, потому как в 90% случаев пустую строку захотят использовать именно в таких проверках.
6-7. Очень спорно, есть или нет смысл отлавливать конкретные исключения, и сможем ли мы их обработать, и вообще — может мы сами себе их накидали и знаем каждый из случаев возможного исключения. Почитайте на досуге, но точного ответа я вам не скажу и каждый, скорее всего, останется при своем мнении.
8. Можно считать за ошибку, только если мы действительно работаем с файлами. Плюс в StringBuilder есть свои особенности работы, которые также придется изучить, чтобы не наделать новых ошибок.
9. Логгирование? Полезно в разной степени для разных продуктов, плюс желательно понимать особенности работы и назначения различных уровней логгирования. Насчет не писать велосипедов — согласен, но даже в общедоступных и признанных решениях можно наступить на огромные грабли.
10. Научитесь работать с TFS? В конце рабочего дня там можно и Shelve операцию сделать для промежуточных результатов. Ну и опять же сильно зависит от размеров команды и схемы работы, но да — НИКОГДА не заливайте в мастер неработающий код.
Итого из 10 мы получаем 2 претензии по синтаксису (1,2), 2 очень спорных момента (6,7), 3 претензии к квалификации сотрудника (3,9,10), и 3 совета не забывать про отдельные конструкции в языке (4,5,8).
PS. Мне тоже показалось, что это какой-то перевод.
crea7or
25.05.2017 23:07+4Это бред, а не перевод.
TheShock
25.05.2017 23:34Это не перевод.
petuhov_k
26.05.2017 05:18+1Серьёзно?
Фразочки вида
Это всего лишь небольшая досада для программиста, она больше влияет на читаемость кода, нежели на его эффективность.
как бынамекают.TheShock
26.05.2017 05:36Вы правы. Я посмотрел, что нету пометки о переводе и потому сделал ложный вывод)
Про фразочки — я просто предположил, что это неудачная попытка подражать заграничным коллегам. Как и ни о чем не говорящая надпись «Эксперт» под псевдонимом автора.
INC_R
25.05.2017 23:17+4Я бы по терминологии высказался.
Но зачем их использовать, если любые переменные класса можно объявить глобальными? Ответ один — свойства дают возможность контролировать как они будут использоваться, чего нельзя отнести к глобальным переменным.
Видимо, под "переменными класса" понимаются поля, а под "глобальными" — публичные. Как можно давать какие-то советы, абсолютно не владея терминологией и превращая публичные поля в глобальные переменные? Это же даже не кривое обозначение чего-либо, это абсолютно разные понятия.
ZOXEXIVO
25.05.2017 23:53+9Приложу-ка я сюда часть исходника .Where
private static IEnumerable<TSource> WhereIterator<TSource>(IEnumerable<TSource> source, Func<TSource, int, bool> predicate) { int index = -1; foreach (TSource element in source) { checked { index++; } if (predicate(element, index)) { yield return element; } } }
Martius
26.05.2017 23:37Linq to Objects конечно не будет быстрее foreach или for, однако есть важный нюанс, так как этот исходник для всего лишь одной перегрузки метода Where(item, index), которая принимает лямбду с двумя аргументами: текущий элемент и его индекс. Что то наподобие:
var numbers = Enumerable.Range(1, 100); numbers.Where((item, index) => { return index % 2 == 0; });
Так что да, эта реализации намного проще. В любом случае, наиболее часто встречаемая перегрузка с одним аргументом намного сложнее и оптимизирование:
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { Iterator<TSource> iterator = source as Iterator<TSource>; if (iterator != null) { return iterator.Where(predicate); } TSource[] array = source as TSource[]; if (array != null) { return array.Length == 0 ? (IEnumerable<TSource>)EmptyPartition<TSource>.Instance : new WhereArrayIterator<TSource>(array, predicate); } List<TSource> list = source as List<TSource>; if (list != null) { return new WhereListIterator<TSource>(list, predicate); } return new WhereEnumerableIterator<TSource>(source, predicate); }
К примеру об оптимизации: если написать довольно типичную цепочку из Where().Select()
var numbers = Enumerable.Range(1, 100); numbers.Where((item, index) => { return index % 2 == 0; }).Select(t=> t.ToString());
то это код выполниться для 100 элементов, а не для 150, как можно было бы подумать, из-за внутренней реализации WhereSelectListIterator.
Полная реализация на githube
itblogger
26.05.2017 06:23+2Плохие советы, негодные.
String.Empty это не константа, поэтому что-то вроде
switch (string) { case String.Empty: //... }
просто не скомпилируется, также как и параметр функции по умолчанию типа
function Foo(string optional = String.Empty) { ... }
Таким образом, если программист использует String.Empty, то в некоторых местах ему все равно придется использовать пустую строку "", разрушая code consistency.
Вредный совет про явное использование типа вместо var. Другие советы не лучше.
Плохая статья, негодная.
Deosis
26.05.2017 06:56+3Эффективный способ:
try { newInteger = int.Parse(astring); } catch (FormatException) { // do something here }
Автор либо тролль, либо редиска. Предложенный вариант мало того, что НЕэффективен, так ещё и не ловит NPE.
if (astring == null || !int.TryParse(astring, out newInteger)) { // do something here }
denismaster
26.05.2017 09:55В официальном codestyle var используется, когда либо тип известен и понятен, т, е например:
var scene = new Scene(...);
Можно также использовать если возвращаемый объект из того же LINQ является анонимным объектом(с другой стороны, это не всегда оправданно, и также зависит от соглашений внутри команды)
Degun
26.05.2017 10:553. Использование глобальных переменных класса вместо свойств
В C# нет глобальных переменных уровня модуля (сборки) в том виде, в каком они есть в C\C++. В C# все переменные находятся в классах. Здесь можно выдвинуть множество предположений, что имелось ввиду. Возможно под глобальными переменными класса подразумевались публичные переменные, определённые в статическом классе уровня сборки. Формулировка автора приводит, по крайней мере, к непониманию материала и как следствие к его отторжению. Это сводит на нет его полезность.
kostus1974
26.05.2017 17:48-1linq? да, надо использовать там, где это логично. и давайте уж тогда измерения, если говорим про эффективность. 100 000 и _сразу_ 1000? а внутри линка что происходит? просто чудо и всё? инопланетная математика срабатывает? нет. полагаю, перебор просто спрятан внутри. linq просто иногда удобнее, но не более того.
общие исключения тоже часто использую. при этом куда-либо вывожу сообщение и запоминаю его в какое-либо свойство объекта. это когда заранее не знаю, что может пойти не так. или когда просто исправить в этой итерации (например) ничего нельзя. да, просто лог ошибки и установка соответствующего возвращаемого значения.
конкретные исключения хороши там, где точно знаешь, что с ними делать (например, можно что-то исправить, или дать конкретный совет пользователю и т.п.).
конечно, здесь нет 10-ти ошибок. но спасибо за напоминание.
TheShock
Предполагаю, что автор в уме себе правильно представляет, но написано довольно мутно и вызывает ощущение: WTF? Грубо говоря, конечно 1000 — более эфективно, но мне надо перебрать все 100 000. Если автор говорил о распаралеливании или еще чем-нибудь, то стоит уточнить.
Что? Не знаю, у меня в большинстве случаев при использовании Linq результат вполне известен. О чем автор?
WTF? Как? Будет ровно тот же NRE при null, а как работать с неожиданным типом данным — вообще слабо представляю. И это какое-то замалчивание ошибки. Как можно использовать var для работы с неизвестным типом данных? Я слаб в C#, объясните мне это, пожалуйста.
Изменять, а не использовать! А читать (тоже использовать, кстати), могут все. Автор, будьте внимания к формулировкам. Ваша статья просто вводит в заблуждение!
qrck13
> Предполагаю, что автор в уме себе правильно представляет, но написано довольно мутно и вызывает ощущение: WTF? Грубо говоря, конечно 1000 — более эфективно, но мне надо перебрать все 100 000. Если автор говорил о распаралеливании или еще чем-нибудь, то стоит уточнить.
Возможно имелся в виду LINQ to SQL, в таком случае будет да, эффективнее, т.к. LINQ запрос транслируется в SQL и исполняется на стороне SQL сервера.
Однако если же опрашивать обычную коллекцию (тот-же List), то ручной цыкл обычно эффективнее, т.к. создает куда меньше оверхеда. Если опрашивать много и мелких списков — то LINQ оказывается сильно неэффективнее.
wowaaa
Слова автора вводят в заблуждение. Единственное, что хотелось бы добавить по этому пункту, без того, что местами проще действительно цикл — если мы над объектами совершаем не одну операцию а множество, то LINQ, скорее всего, будет быстрее за счет оптимизаций разложения в Expression Tree.
mayorovp
Нет там никаких оптимизаций если говорить именно про Linq 2 Objects.
dmitry_dvm
Ну есть ситуации, когда нужно использовать именно Linq даже на мелких списках в силу его ленивого вычисления.
Lofer
Вырывать руки надо за «неожиданный тип данных» программеру. Ну а за магию «избежать ошибок при выполнении кода, в случае, если результатом является NULL» — вообще позвоночник ломать…
Голова зачем дана ?! кушать в нее ?!
TheShock
Ну это, в целом понятно. Но даже чисто теоретически интересно, как var помогает работать с «неизвестным» типом данных? Это ж не dynamic.
И уж тем более интересно чем отличается получение null в переменную с var в сравнении с получением null в переменную с явным указанием типа.
mayorovp
Я так понимаю, под "неизвестным" типом данных понимался анонимный.
Gradarius
Это достойный заголовок данной статьи.
Каждый пункт вызывал недоумение, а последний совсем добил и это в статье «10 распространенных ошибок при программировании на C#».