Код с ошибками на C#: 10 самых распространенных ошибок в программировании на C#

Опубликовано: 2022-03-11

О Си-Шарп

C# — один из нескольких языков, предназначенных для среды выполнения Microsoft Common Language Runtime (CLR). Языки, ориентированные на среду CLR, выигрывают от таких функций, как межъязыковая интеграция и обработка исключений, повышенная безопасность, упрощенная модель взаимодействия компонентов, а также службы отладки и профилирования. Из современных языков CLR C# наиболее широко используется для сложных профессиональных проектов разработки, предназначенных для настольных, мобильных или серверных сред Windows.

C# — это объектно-ориентированный язык со строгой типизацией. Строгая проверка типов в C#, как во время компиляции, так и во время выполнения, приводит к тому, что о большинстве типичных ошибок программирования C# сообщается как можно раньше, а их местонахождение определяется довольно точно. Это может сэкономить много времени при программировании на C Sharp по сравнению с отслеживанием причин загадочных ошибок, которые могут возникнуть спустя долгое время после того, как нарушающая операция выполняется в языках, которые более либеральны в отношении обеспечения безопасности типов. Однако многие программисты C# невольно (или по невнимательности) отказываются от преимуществ этого обнаружения, что приводит к некоторым проблемам, обсуждаемым в этом руководстве по C#.

Об этом учебнике по программированию на C Sharp

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

Хотя большинство ошибок, обсуждаемых в этой статье, характерны для C#, некоторые из них также относятся к другим языкам, предназначенным для CLR или использующим библиотеку классов Framework (FCL).

Распространенная ошибка программирования на C# №1: использование ссылки вместо значения или наоборот

Программисты C++ и многих других языков привыкли контролировать, являются ли значения, которые они присваивают переменным, просто значениями или ссылками на существующие объекты. Однако в программировании на C Sharp это решение принимает программист, написавший объект, а не программист, создающий экземпляр объекта и присваивающий его переменной. Это распространенная ошибка для тех, кто пытается изучить программирование на C#.

Если вы не знаете, является ли используемый вами объект типом значения или ссылочным типом, вы можете столкнуться с некоторыми сюрпризами. Например:

 Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue

Как вы можете видеть, объекты Point и Pen были созданы одинаково, но значение point1 осталось неизменным, когда новое значение координаты X было присвоено point2 , тогда как значение pen1 было изменено, когда новый цвет был назначен для point2. pen2 . Таким образом, мы можем сделать вывод , что каждая точка point1 и point2 содержит собственную копию объекта Point , тогда как pen1 и pen2 содержат ссылки на один и тот же объект Pen . Но как мы можем узнать это, не проведя этот эксперимент?

Ответ заключается в том, чтобы посмотреть определения типов объектов (что вы можете легко сделать в Visual Studio, поместив курсор на имя типа объекта и нажав F12):

 public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type

Как показано выше, в программировании на C# ключевое слово struct используется для определения типа значения, а ключевое слово class используется для определения ссылочного типа. Для тех, кто работал с C++ и усыпил ложное чувство безопасности из-за сходства между ключевыми словами C++ и C#, такое поведение, скорее всего, станет неожиданностью и может заставить вас обратиться за помощью к учебнику по C#.

Если вы собираетесь зависеть от какого-либо поведения, которое различается между типами значений и ссылочными типами — например, возможность передать объект в качестве параметра метода и заставить этот метод изменить состояние объекта — убедитесь, что вы имеете дело с правильный тип объекта, чтобы избежать проблем с программированием на C#.

Распространенная ошибка программирования на C# №2: неправильное понимание значений по умолчанию для неинициализированных переменных

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

 class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }

Почему точка 1 не point1 нулю? Ответ заключается в том, что Point — это тип значения, а значение по умолчанию для Point равно (0,0), а не null. Неспособность распознать это — очень простая (и распространенная) ошибка в C#.

Многие (но не все) типы значений имеют свойство IsEmpty , которое можно проверить, чтобы убедиться, что оно равно значению по умолчанию:

 Console.WriteLine(point1.IsEmpty); // True

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

Распространенная ошибка программирования на C# № 3: использование неправильных или неуказанных методов сравнения строк

В C# существует множество различных способов сравнения строк.

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

Вместо этого предпочтительным способом проверки равенства строк в программировании на C# является метод Equals :

 public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);

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

Тем не менее, использование сигнатуры метода Equals , которая включает в себя comparisonType каждый раз при сравнении строк, не только сделает ваш код более понятным, но и заставит вас явно задуматься о том, какой тип сравнения вам нужно сделать. Это стоит сделать, потому что даже если английский язык не может обеспечить много различий между порядковыми и учитывающими культуру сравнениями, другие языки предоставляют множество, и игнорирование возможностей других языков открывает перед вами большой потенциал для ошибки в пути. Например:

 string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));

Самый безопасный способ — всегда предоставлять параметр comparisonType для метода Equals . Вот несколько основных рекомендаций:

  • При сравнении строк, которые были введены пользователем или должны отображаться пользователю, используйте сравнение с учетом языка и региональных параметров ( CurrentCulture или CurrentCultureIgnoreCase ).
  • При сравнении программных строк используйте порядковое сравнение ( Ordinal или OrdinalIgnoreCase ).
  • InvariantCulture и InvariantCultureIgnoreCase обычно не следует использовать, за исключением очень ограниченного числа случаев, поскольку порядковые сравнения более эффективны. Если необходимо сравнение с учетом культуры, его обычно следует выполнять с текущей культурой или другой конкретной культурой.

В дополнение к методу Equals строки также предоставляют метод Compare , который дает вам информацию об относительном порядке строк, а не только проверку на равенство. Этот метод предпочтительнее операторов < , <= , > и >= по тем же причинам, которые обсуждались выше, — чтобы избежать проблем с C#.

Связанный: 12 основных вопросов для интервью по .NET

Распространенная ошибка программирования на C# № 4: использование итерационных (вместо декларативных) операторов для управления коллекциями

В C# 3.0 добавление языковых запросов (LINQ) к языку навсегда изменило способ запроса коллекций и управления ими. С тех пор, если вы используете итерационные операторы для управления коллекциями, вы не использовали LINQ, когда вам, вероятно, следовало бы это сделать.

Некоторые программисты на C# даже не знают о существовании LINQ, но, к счастью, их число становится все меньше. Однако многие по-прежнему думают, что из-за сходства между ключевыми словами LINQ и операторами SQL его можно использовать только в коде, который запрашивает базы данных.

Хотя операторы LINQ очень часто используются для запросов к базе данных, они фактически работают с любой перечисляемой коллекцией (т. е. с любым объектом, реализующим интерфейс IEnumerable). Так, например, если у вас есть массив учетных записей, вместо написания списка C# foreach:

 decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }

можно было просто написать:

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

Хотя это довольно простой пример того, как избежать этой распространенной проблемы программирования на C#, бывают случаи, когда один оператор LINQ может легко заменить десятки операторов в итеративном цикле (или вложенных циклах) в вашем коде. А менее общий код означает меньше возможностей для внесения ошибок. Имейте в виду, однако, что может быть компромисс с точки зрения производительности. В критических для производительности сценариях, особенно когда ваш итеративный код может делать предположения о вашей коллекции, которые не может сделать LINQ, обязательно сравните производительность двух методов.

Распространенная ошибка программирования на C# № 5: Неспособность учесть базовые объекты в операторе LINQ

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

Например, рассмотрим следующее утверждение:

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

Что произойдет, если один из объектов account.Status будет равен «Активный» (обратите внимание на заглавную букву А)? Ну, если бы myAccounts был объектом DbSet (который был настроен с конфигурацией по умолчанию без учета регистра), выражение where все равно соответствовало бы этому элементу. Однако, если бы myAccounts находился в массиве в памяти, он не совпадал бы и, следовательно, давал бы другой результат для итогового значения.

Но подождите минутку. Когда мы говорили о сравнении строк ранее, мы видели, что оператор == выполняет порядковое сравнение строк. Так почему же в этом случае оператор == выполняет сравнение без учета регистра?

Ответ заключается в том, что когда базовые объекты в инструкции LINQ являются ссылками на данные таблицы SQL (как в случае с объектом Entity Framework DbSet в этом примере), инструкция преобразуется в инструкцию T-SQL. Затем операторы следуют правилам программирования T-SQL, а не правилам программирования C#, поэтому сравнение в приведенном выше случае оказывается нечувствительным к регистру.

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

Распространенная ошибка программирования на C# № 6: запутаться или обмануть методы расширения

Как упоминалось ранее, операторы LINQ работают с любым объектом, который реализует IEnumerable. Например, следующая простая функция суммирует балансы на любом наборе счетов:

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }

В приведенном выше коде тип параметра myAccounts объявлен как IEnumerable<Account> . Поскольку myAccounts ссылается на метод Sum (C# использует знакомую «точечную нотацию» для ссылки на метод класса или интерфейса), мы ожидаем увидеть метод Sum() в определении интерфейса IEnumerable<T> . Однако определение IEnumerable<T> не ссылается на какой-либо метод Sum и просто выглядит так:

 public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }

Так где же определен метод Sum() ? Язык C# строго типизирован, поэтому, если бы ссылка на метод Sum была недействительной, компилятор C# обязательно пометил бы это как ошибку. Следовательно, мы знаем, что оно должно существовать, но где? Кроме того, где определения всех других методов, которые предоставляет LINQ для запроса или агрегирования этих коллекций?

Ответ заключается в том, что Sum() не является методом, определенным в интерфейсе IEnumerable . Скорее, это статический метод (называемый «метод расширения»), который определен в классе System.Linq.Enumerable :

 namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }

Так что же отличает метод расширения от любого другого статического метода и что позволяет нам получить к нему доступ в других классах?

Отличительной чертой метода расширения является модификатор this в его первом параметре. Это «волшебство», которое идентифицирует его для компилятора как метод расширения. Тип параметра, который он изменяет (в данном случае IEnumerable<TSource> ), обозначает класс или интерфейс, который затем появится для реализации этого метода.

(Кроме того, нет ничего волшебного в сходстве между именем интерфейса IEnumerable и именем класса Enumerable , для которого определен метод расширения. Это сходство — просто произвольный стилистический выбор.)

При таком понимании мы также можем видеть, что функцию sumAccounts , которую мы представили выше, вместо этого можно было бы реализовать следующим образом:

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }

Тот факт, что мы могли бы реализовать это таким образом, вместо этого поднимает вопрос, зачем вообще нужны методы расширения? Методы расширения — это, по сути, удобство языка программирования C#, позволяющее «добавлять» методы к существующим типам без создания нового производного типа, перекомпиляции или иного изменения исходного типа.

Методы расширения становятся доступными благодаря включению using [namespace]; заявление в верхней части файла. Вам нужно знать, какое пространство имен C# включает в себя методы расширения, которые вы ищете, но это довольно легко определить, если вы знаете, что именно вы ищете.

Когда компилятор C# встречает вызов метода для экземпляра объекта и не находит этот метод, определенный в классе объекта, на который указывает ссылка, он затем просматривает все методы расширения, которые находятся в пределах области видимости, чтобы попытаться найти тот, который соответствует требуемому методу. подпись и класс. Если он найдет его, он передаст ссылку на экземпляр в качестве первого аргумента этому методу расширения, а затем остальные аргументы, если таковые имеются, будут переданы в качестве последующих аргументов в метод расширения. (Если компилятор C# не найдет никакого соответствующего метода расширения в области видимости, он выдаст ошибку.)

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

Хотя у использования методов расширения, безусловно, есть преимущества, они могут вызвать проблемы и призывы о помощи по программированию на C# для тех разработчиков, которые не знают о них или не понимают их должным образом. Это особенно верно при просмотре примеров кода в Интернете или любого другого заранее написанного кода. Когда такой код вызывает ошибки компилятора (поскольку он вызывает методы, явно не определенные в классах, для которых они вызываются), возникает тенденция думать, что код применим к другой версии библиотеки или вообще к другой библиотеке. Много времени может быть потрачено на поиск новой версии или фантомной «пропавшей библиотеки», которой не существует.

Даже разработчики, знакомые с методами расширения, иногда попадают в ловушку, когда в объекте есть метод с таким же именем, но его сигнатура метода слегка отличается от сигнатуры метода расширения. Много времени может быть потрачено впустую на поиск опечатки или ошибки, которой просто нет.

Использование методов расширения в библиотеках C# становится все более распространенным. В дополнение к LINQ, блок приложений Unity и платформа веб-API являются примерами двух широко используемых современных библиотек Microsoft, которые также используют методы расширения, и есть много других. Чем современнее фреймворк, тем больше вероятность того, что он будет включать в себя методы расширения.

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

Распространенная ошибка программирования на C# № 7: использование неподходящего типа коллекции для текущей задачи

C# предоставляет большое разнообразие объектов коллекций, среди которых приведен лишь неполный список:
Array , ArrayList , BitArray , BitVector32 , Dictionary<K,V> , HashTable , HybridDictionary , List<T> , NameValueCollection , OrderedDictionary , Queue, Queue<T> , SortedList , Stack, Stack<T> , StringCollection , StringDictionary .

Хотя могут быть случаи, когда слишком много вариантов так же плохо, как и недостаточно, это не относится к объектам-коллекциям. Количество доступных вариантов определенно может работать в вашу пользу. Потратьте немного больше времени заранее, чтобы изучить и выбрать оптимальный тип коллекции для ваших целей. Скорее всего, это приведет к лучшей производительности и меньшему количеству ошибок.

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

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

Другой распространенной проблемой C# является написание собственного объекта коллекции. Это не значит, что это никогда не уместно, но с таким полным выбором, который предлагает .NET, вы, вероятно, сможете сэкономить много времени, используя или расширяя уже существующий, вместо того, чтобы изобретать велосипед. В частности, C5 Generic Collection Library для C# и CLI предлагает широкий набор дополнительных готовых коллекций, таких как постоянные древовидные структуры данных, приоритетные очереди на основе кучи, списки хэш-индексированных массивов, связанные списки и многое другое.

Распространенная ошибка программирования на C# № 8: пренебрежение свободными ресурсами

В среде CLR используется сборщик мусора, поэтому вам не нужно явно освобождать память, созданную для какого-либо объекта. На самом деле, вы не можете. В C нет эквивалента оператору delete C++ или функции free() . Но это не значит, что вы можете просто забыть обо всех объектах после того, как закончили их использовать. Многие типы объектов инкапсулируют некоторые другие типы системных ресурсов (например, файл на диске, соединение с базой данных, сетевой сокет и т. д.). Если оставить эти ресурсы открытыми, общее количество системных ресурсов может быстро истощиться, что снизит производительность и в конечном итоге приведет к сбоям в работе программы.

Хотя метод деструктора можно определить в любом классе C#, проблема с деструкторами (также называемыми финализаторами в C#) заключается в том, что вы не можете точно знать, когда они будут вызваны. Они вызываются сборщиком мусора (в отдельном потоке, что может вызвать дополнительные сложности) в неопределенное время в будущем. Попытка обойти эти ограничения путем принудительной сборки мусора с помощью GC.Collect() не является лучшей практикой C#, так как это заблокирует поток на неизвестное время, пока он собирает все объекты, подходящие для сбора.

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

Утечки ресурсов являются проблемой практически в любой среде. Однако C# предоставляет надежный и простой в использовании механизм, который, если его использовать, может сделать утечки гораздо более редкими. Платформа .NET определяет интерфейс IDisposable , который состоит исключительно из метода Dispose() . Любой объект, который реализует IDisposable , ожидает, что этот метод будет вызываться всякий раз, когда потребитель объекта заканчивает манипулировать им. Это приводит к явному детерминированному освобождению ресурсов.

Если вы создаете и удаляете объект в контексте одного блока кода, в принципе непростительно забыть вызвать Dispose() , потому что C# предоставляет оператор using , который гарантирует вызов Dispose( Dispose() независимо от того, как блок кода завершается (будь то исключение, оператор возврата или просто закрытие блока). И да, это тот же оператор using , упомянутый ранее, который используется для включения пространств имен C# в начало вашего файла. У него есть вторая, совершенно не связанная с этим цель, о которой многие разработчики C# не подозревают; а именно, чтобы гарантировать, что Dispose() вызывается для объекта при выходе из блока кода:

 using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }

Создавая блок using в приведенном выше примере, вы точно знаете, что myFile.Dispose() будет вызываться, как только вы закончите работу с файлом, независимо от того, генерирует ли Read() исключение.

Распространенная ошибка программирования на C# № 9: избегание исключений

C# продолжает обеспечивать безопасность типов во время выполнения. Это позволяет гораздо быстрее выявлять многие типы ошибок в C#, чем в таких языках, как C++, где ошибочные преобразования типов могут привести к присвоению произвольных значений полям объекта. Однако, опять же, программисты могут растратить эту замечательную функцию, что приведет к проблемам с C#. Они попадают в эту ловушку, потому что C# предоставляет два разных способа выполнения действий: один может генерировать исключение, а другой — нет. Некоторые будут уклоняться от маршрута исключения, полагая, что отсутствие необходимости писать блок try/catch сэкономит им часть кода.

Например, вот два разных способа выполнения явного приведения типа в C#:

 // METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;

Наиболее очевидной ошибкой, которая может возникнуть при использовании метода 2, будет невозможность проверить возвращаемое значение. Это, вероятно, приведет к возможному исключению NullReferenceException, которое может появиться гораздо позже, что значительно усложнит отслеживание источника проблемы. Напротив, метод 1 немедленно вызвал бы InvalidCastException , что сделало бы источник проблемы более очевидным.

Более того, даже если вы не забудете проверить возвращаемое значение в методе 2, что вы будете делать, если обнаружите, что оно равно нулю? Является ли метод, который вы пишете, подходящим местом для сообщения об ошибке? Есть ли что-то еще, что вы можете попробовать, если этот состав не сработает? Если нет, то создание исключения является правильным решением, поэтому вы можете позволить этому произойти как можно ближе к источнику проблемы.

Вот несколько примеров других распространенных пар методов, где один генерирует исключение, а другой нет:

 int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty

Некоторые разработчики C# настолько «неприемлемы для исключений», что автоматически предполагают, что метод, который не генерирует исключение, лучше. Хотя в некоторых избранных случаях это может быть правдой, это совсем не правильно в качестве обобщения.

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

 if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }

вместо:

 try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }

Однако неверно предполагать, что TryParse обязательно является «лучшим» методом. Иногда это так, иногда нет. Вот почему есть два способа сделать это. Используйте правильный для контекста, в котором вы находитесь, помня, что исключения, безусловно, могут быть вашими друзьями как разработчика.

Распространенная ошибка программирования на C# № 10: накопление предупреждений компилятора

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

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

Обычный простой пример для этого руководства по программированию на C# — это когда вы изменяете свой алгоритм, чтобы исключить использование переменной, которую вы использовали, но вы забыли удалить объявление переменной. Программа будет работать нормально, но компилятор отметит бесполезное объявление переменной. Тот факт, что программа работает идеально, заставляет программистов пренебрегать устранением причины предупреждения. Кроме того, кодировщики пользуются функцией Visual Studio, которая позволяет им легко скрывать предупреждения в окне «Список ошибок», чтобы они могли сосредоточиться только на ошибках. Вскоре появляются десятки предупреждений, и все они благополучно игнорируются (или, что еще хуже, скрыты).

Но если вы проигнорируете предупреждение такого типа, рано или поздно что-то вроде этого вполне может попасть в ваш код:

 class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }

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

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

Помните, что компилятор C Sharp дает вам много полезной информации о надежности вашего кода… если вы слушаете. Не игнорируйте предупреждения. Обычно их исправление занимает всего несколько секунд, а исправление новых, когда они происходят, может сэкономить вам часы. Приучите себя ожидать, что в окне «Список ошибок» Visual Studio отобразится «0 ошибок, 0 предупреждений», чтобы любые предупреждения вызывали у вас достаточно дискомфорта, чтобы вы могли немедленно их устранить.

Конечно, из каждого правила есть исключения. Соответственно, могут быть моменты, когда ваш код будет выглядеть подозрительно для компилятора, даже если он именно такой, каким вы его хотели видеть. В этих очень редких случаях используйте #pragma warning disable [warning id] только вокруг кода, который вызывает предупреждение, и только для идентификатора предупреждения, который он вызывает. Это подавит это предупреждение и только это предупреждение, так что вы все равно сможете быть в курсе новых.

Заворачивать

C# — это мощный и гибкий язык со множеством механизмов и парадигм, которые могут значительно повысить производительность. Однако, как и в случае с любым программным инструментом или языком, ограниченное понимание или оценка его возможностей иногда может быть скорее препятствием, чем преимуществом, оставляя человека в пресловутом состоянии «знания достаточно, чтобы быть опасным».

Использование учебника C Sharp, подобного этому, для ознакомления с ключевыми нюансами C#, такими как (но не ограничиваясь ими) проблемы, поднятые в этой статье, поможет в оптимизации C#, избегая при этом некоторых из наиболее распространенных ловушек язык.


Дальнейшее чтение в блоге Toptal Engineering:

  • Основные вопросы на собеседовании по C#
  • C# против C++: что лежит в основе?