Предлагаю вашему вниманию перевод статьи о «ловушках» в С#. Данная статья будет полезна начинающим программистам пока еще не знакомым с тонкостями языка.

Приятного чтения.



«Это все мелочи, мелочи. Но нет ничего важнее мелочей.»

??

1. Использование ошибочного типа коллекции


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

> Подробнее здесь

2. Неиспользование yield return


При перечислении объектов для вызова следует использовать оператор yield return, а не создавать возвращающую коллекцию(прим.переводчика: зависит от ситуации). Преимущества этого шаблона:

  • не нужно хранить целую коллекцию в памяти (она может быть очень большой)
  • yield return непосредственно возвращает управление вызвавшей функции после каждой итерации
  • идет обработка только тех результатов, которые будут использоваться (зачем перебирать всю коллекцию, если вызывающая функция хочет получить первое значение)

3. Анализ двусмысленных дат


Обязательно нужно указывать формат, если речь идет об анализе неоднозначных дат. Например, в строке «03/04/05» непонятно, что является днем, что месяцем, а что годом, и это может привести к серьезным ошибкам для пользователя.

Здесь используйте DateTime.ParseExact / DateTimeOffset.ParseExact для предоставления спецификатора формата:

var date = DateTime.ParseExact("01/12/2000", "MM/dd/yyyy", null)

4. Повторная обработка исключения с его экземпляром


Если вы хотите поймать и повторно обработать исключение, обязательно используйте простой throw. Если вы вдруг используете throw ex, он не будет сохранять стек вызовов исключения.

Используйте:


catch(SomeException ex)
{
    logger.log(ex);
    throw;
}

И не используйте:


catch(SomeException ex)
{
    logger.log(ex);
    throw ex;
}

5. Обращение к виртуальным компонентам в конструкторе


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

Например:


public class Parent
{
  public Parent()
  {
    Console.WriteLine("Parent Ctor");
    Method();
  }

  public virtual void Method()
  {
    Console.WriteLine("Parent method");
  }
}

public class Child : Parent
{
  public Child()
  {
    Console.WriteLine("Child Ctor");
  }

  public override void Method()
  {
    Console.WriteLine("Child method");
  }
}

Инициализация дочернего класса будет выглядеть следующим образом:

  1. Родительский конструктор
  2. Дочерний метод
  3. Дочерний конструктор

Т.е. дочерний метод вызывается перед дочерним конструктором.

6. Исключения в статическом конструкторе


Если класс генерирует исключение в статическом конструкторе, он делает класс бесполезным. Даже нестатическая конструкция будет невозможна. Класс будет генерировать System.TypeInitializationException всякий раз, когда на него ссылаются (статически или нет).

7. Необязательные параметры во внешней сборке


Необязательные параметры могут работать не так, как ожидалось, если на них ссылаться из внешней сборки. Скажем, у вас есть некоторый функционал, упакованный в DLL. И, скажем, вы хотите внести незначительные изменения в код (изменить необязательный параметр на другое значение).

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

> Подробнее здесь

8. Универсальные методы с неуниверсальной перегрузкой


Скажем, вы имеете универсальный метод, принимающий параметр типа Т и еще один метод с тем же именем, но принимающий параметр конкретного типа. Компилятор будет выбирать более подходящий метод для каждого типа параметра, и явно определённый тип более корректен, чем универсальный.

Например, есть следующий класс:


class GenericTest
{
  public void Test<T>(T parameter)
  {
    Console.WriteLine("Generic method!");
  }

  public void Test(string parameter)
  {
    Console.WriteLine("Non-generic method!");
  }
}

И следующий код…


var instance = new GenericTest();
instance.Test(7);
instance.Test("foo");

Выдаст вам результат:


Generic method!
Non-generic method!

Т.е. компилятор выбирает более специфический строковый метод при втором вызове.

9. Изменение хэш-кода после добавления объекта в словарь


Словари зависят от значений ключей, возвращенных методом Object.GetHashCode(). Это означает, что хэш-код ключа не может быть изменен после добавления в словарь.

10. ValueType.Equals будет тормозить, если структура содержит компонент-ссылку


Убедитесь, что ваша структура не содержит компонент-ссылку, если вы хотите использовать ValueType.Equals для сравнения двух экземпляров. Медленная работа связана с тем, что ValueType.Equals будет использовать рефлексию, чтобы определить ссылочный компонент, а это может быть немного медленнее, чем ожидалось.

Автор оригинала: Robert Bengtsson
Поделиться с друзьями
-->

Комментарии (37)


  1. lair
    22.02.2017 14:20
    +6

    При перечислении объектов для вызова следует использовать оператор yield return, а не создавать возвращающую коллекцию.

    "В зависимости от ситуации" явно пропущено.


    1. GetShuk
      22.02.2017 14:28

      В оригинале данной вставки нет, но замечание справедливое. Спасибо.


  1. mayorovp
    22.02.2017 14:27
    +4

    Пункт 2 — сомнителен. Активное использование ленивых вычислений (а yield return — это ленивые вычисления) может привести к тому, что исключение произойдет совсем не там где оно ожидалось.


    yield return сам по себе — лучше чем создание коллекции только чтобы ее вернуть — но бросаться переписывать уже готовую программу не следует. Могут быть сюрпризы.


    По пункту 4 — забыли рассказать что делать если исключение нужно сохранить, а потом бросать заново уже в другом месте. А нужно сохранять не исключение, а ExceptionDispatchInfo. На новых фреймворках.


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


    По пункту 8. Так в чем ошибка или ловушка-то заключаются? Это же очевидное ожидаемое поведение...


  1. atykhyy
    22.02.2017 14:58
    +2

    он рендерит бесполезный класс
    На экран, что ли, рендерит? :) Правильно: делает класс бесполезным. Учите английский.


    1. GetShuk
      22.02.2017 14:59

      Хотелось как лучше, а получилось как всегда XD.
      Исправлено, спасибо.


  1. Mak_71_rus
    22.02.2017 15:11

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


  1. AllexIn
    22.02.2017 15:25
    +5

    Т.е. компилятор выбирает более специфический строковый метод при втором вызове.

    И что здесь не так?
    Универсальный метод — для вообще всех данных.
    А не универсальный — для обработки с конкретными типами.

    Ловушка в чем? В том, что мы сделали метод для строки и он вызывается… для строк?
    А какого поведения здесь ждет программист?


    1. ad1Dima
      22.02.2017 19:26

      Когда знаешь как работают generic, понимаешь, что компилятор еще один метод с той же сигнатурой не создаст.


      А когда для тебя это магическая функция, принимающая что угодно...


      1. AllexIn
        22.02.2017 19:27

        Я просто не понимаю, какое альтернативное поведение может здесь ждать программист.


        1. ad1Dima
          22.02.2017 19:38

          Что у тебя две функции и обе принимают string.


          1. AllexIn
            22.02.2017 19:39

            и по какому принципу по мнению этого программиста компилятор выбирает какую функцию использовать?


            1. ad1Dima
              22.02.2017 19:54

              Вот тут и ловушка. Это ж магическое программирование. В зависимости от ситуации.


              1. PsyHaSTe
                22.02.2017 20:25

                Какая магия? Все завист от типы ссылки. Здесь не больше магии, чем в перекрытых методах.


                1. ad1Dima
                  22.02.2017 20:50

                  Вы мне объясняете, как дженерики работают? Я знаю. Я объясняю, где тут может быть проблема в восприятии


                  1. PsyHaSTe
                    22.02.2017 23:23

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


                    1. ad1Dima
                      24.02.2017 08:51

                      Есть целые фирмы, где работают StackOverflow-программисты. Если вы с такими не сталкивались никогда, вам повезло.


  1. wlbm_onizuka
    22.02.2017 15:33

    Пятый пункт интересный. Становится понятно зачем нужен инициализатор типа)
    в том смысле что объявленные в классе поля все равно успевают инициализироваться, несмотря на то что конструктор не отрабатывает


  1. Oxoron
    22.02.2017 17:00

    Кто-нибудь знает, почему сравнение Value-Types медленное? Почему нельзя построить сравнивающий Expression Tree при первом вызове сравнения структур данного типа и закешировать его?


    1. lair
      22.02.2017 17:05

      Почему нельзя построить сравнивающий Expression Tree при первом вызове сравнения структур данного типа и закешировать его?

      А зачем? Если у вас в value type есть ссылочные поля, логика Equals, скорее всего, прикладная, а в этом случае вам все равно надо писать свою реализацию.


      А сравнение value types, состоящих из value types — быстрое.


  1. Nihau
    22.02.2017 17:47
    +3

    ValueType.Equals будет использовать отражение

    Рефлексия.


    1. GetShuk
      22.02.2017 17:55

      https://msdn.microsoft.com/ru-ru/library/bsc2ak47(v=vs.110).aspx
      В англоязычной: reflection
      В русскоязычной: отражение
      Или русскоязычная документация ошибочна?


      1. RoboNET
        22.02.2017 17:59
        +2

        Там написано в самом верху

        Данная статья переведена с помощью средств машинного перевода.


        1. GetShuk
          22.02.2017 18:10

          Принято, исправлю.
          Спасибо.


        1. mayorovp
          22.02.2017 18:36

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


          1. GetShuk
            22.02.2017 18:46

            Буду знать :)


          1. Deosis
            23.02.2017 10:44

            Скажите это гуртовщикам мыши.


    1. wlbm_onizuka
      22.02.2017 19:16
      +2

      Оба термина широко-употребимы и все их понимают, слух не режет вроде.


      1. alex_zzzz
        23.02.2017 00:13
        +1

        Мне режет, потому что каждый раз представляю себе зеркало. Есть два адекватных варианта перевода: рефлексия и интроспекция.


        1. uterr
          23.02.2017 12:45

          «отражение» почти никогда не слышал в этом смысле


  1. PsyHaSTe
    22.02.2017 20:31

    10 пункт странный. Если структуры нужно сравнивать, то в любом случае нужно реализовывать IEquitable<T>, а рассуждать, что метод в 10 раз медленнее лучше, чем в 100 раз медленнее особого смысла нет.


    А во-вторых, вот нужно мне стрингу засунуть в структуру, которую я хочу сравнивать — автор по факту говорит мне, что структуру в таком случае использовать нельзя :) Хотя все решается тривиально. По сути фреймворк просто зря имеет Equals и GetHashCode в object'е. С другой стороны, я понимаю, зачем это сделано — тут начинаются возня с мониторами, локами, сохранением состояния блокировки в хэше объекта и прочие прелести… То есть есть причины, почему все это в object'е хранится. Но по сути все кастомные структуры со сравнением обязаны реализовывать IEquitable<T>. Можно хоть roslyn-анализатор ставить, чтобы Equals вызывался исключительно интерфейсный, а не object'овый.


    1. mayorovp
      22.02.2017 21:08

      Лучше roslyn-генератор, чтобы дописывал нормальную реализацию :)


      1. PsyHaSTe
        22.02.2017 23:23
        +1

        Это типичный АОП, стоит вспомнить те же выводимые конструкторы из Rust. Всё к этому идет, смысла писать каждый раз бойлерплейт код никакого нет.


    1. Deosis
      23.02.2017 10:47

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


  1. lany
    23.02.2017 14:04

    1, 5, 6, 8 и 9 справедливы и для Java. Всё же несильно языки различаются =)


  1. ReaderReader
    23.02.2017 16:29

    Номер 8 на мой взгляд, очень странный. Как раз частичная специализация шаблонов — это очень сильная фича при правильном использвоании. В качестве примера можно посмотреть у Александреску


    1. mayorovp
      23.02.2017 21:02

      Частичной специализации шаблона в C# нет. Номер 8 — это простая перегрузка метода.


    1. imanushin
      24.02.2017 14:29
      +1

      Неоднозначность может проявляться вот в таких случаях:


      public static void ExecuteTest<TValue>(TValue value)
      {
           GenericTest.Test(value);
      }
      
      public static void Foo()
      {
           ExecuteTest(123); // Generic method
           ExecuteTest("123"); // Generic method - все же поняли, что это не С++?
      }