Как стать автором
Обновить
Veeam Software
Продукты для резервного копирования информации

Занимательный C#. Пять примеров для кофе-брейка

Время на прочтение9 мин
Количество просмотров28K
Написав уже не одну статью про Veeam Academy, мы решили приоткрыть немного внутренней кухни и предлагаем вашему вниманию несколько примеров на C#, которые мы разбираем с нашими студентами. При их составлении мы отталкивались от того, что наша аудитория — это начинающие разработчики, но и опытным программистам тоже может быть интересно заглянуть под кат. Наша цель — показать, насколько глубока кроличья нора, параллельно объясняя особенности внутреннего устройства C#.

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

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

image

Пример 1


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

Наш первый пример – пример на внимательность и на знание того, во что разворачивается блок using. А также вполне себе тема для общения во время собеседования.

Рассмотрим код:

    public struct SDummy : IDisposable
    {
        private bool _dispose;
        
        public void Dispose()
        {
            _dispose = true;
        }

        public bool GetDispose()
        {
            return _dispose;
        }

        static void Main(string[] args)
        {
            var d = new SDummy();

            using (d)
            {
                Console.WriteLine(d.GetDispose());
            }

            Console.WriteLine(d.GetDispose());
        }
    }

Что выведет на консоль метод Main?
Обратим внимание, что SDummy – это структура, реализующая интерфейс IDisposable, благодаря чему переменные типа SDummy можно использовать в блоке using.

Согласно спецификации языка C# using statement для значимых типов во время компиляции разворачивается в try-finally блок:

    try
    {
        Console.WriteLine(d.GetDispose());
    }
    finally
    {
        ((IDisposable)d).Dispose();
    }

Итак, в нашем коде внутри блока using вызывается метод GetDispose(), который возвращает булевское поле _dispose, значение которого для объекта d еще нигде не было задано (оно задается только в методе Dispose(), который еще не был вызван) и поэтому возвращается значение по умолчанию, равное False. Что дальше?

А дальше самое интересное.
Выполнение строки в блоке finally
 ((IDisposable)d).Dispose();

в обычном случае приводит к упаковке (boxing). Это нетрудно увидеть, например, тут (справа вверху в Results сначала выберите C#, а потом IL):

image

В этом случае метод Dispose вызывается уже для другого объекта, а вовсе не для объекта d.
Запустим нашу программу и увидим, что программа действительно выводит на консоль «False False». Но все ли так просто? :)

На самом деле никакой УПАКОВКИ НЕ ПРОИСХОДИТ. Что, по словам Эрика Липперта, сделано ради оптимизации (см. тут и тут).
Но, если нет упаковки (что само по себе может показаться удивительным), почему на экране «False False», а не «False True», ведь Dispose теперь должен применяться к тому же объекту?!?

А вот и не к тому же!
Заглянем, во что разворачивает нашу программу компилятор C#:

public struct SDummy : IDisposable
{
    private bool _dispose;

    public void Dispose()
    {
        _dispose = true;
    }

    public bool GetDispose()
    {
        return _dispose;
    }

    private static void Main(string[] args)
    {
        SDummy sDummy = default(SDummy);
        SDummy sDummy2 = sDummy;
        try
        {
            Console.WriteLine(sDummy.GetDispose());
        }
        finally
        {
            ((IDisposable)sDummy2).Dispose();
        }
        Console.WriteLine(sDummy.GetDispose());
    }
}


Появилась новая переменная sDummy2, к которой применяется метод Dispose()!
Откуда взялась эта скрытая переменная?
Снова обратимся к спеке:
A using statement of the form 'using (expression) statement' has the same three possible expansions. In this case ResourceType is implicitly the compile-time type of the expression… The 'resource' variable is inaccessible in, and invisible to, the embedded statement.

Т.о. переменная sDummy оказывается невидимой и недоступной для встроенного выражения (embedded statement) блока using, а все операции внутри этого выражения производятся с другой переменной sDummy2.

В итоге метод Main выводит на консоль «False False», а не «False True», как считают многие из тех, кто столкнулся с этим примером впервые. При этом обязательно имейте в виду, что тут не происходит упаковки, но создается дополнительная скрытая переменная.

Общий вывод такой: изменяемые значимые типы (mutable value types) – это зло, которое лучше избегать.

Похожий пример рассмотрен тут. Если тема интересна, очень рекомендуем заглянуть.

Хочется сказать отдельное спасибо SergeyT за ценные замечания к этому примеру.



Пример 2


Конструкторы и последовательность их вызовов – одна из основных тем любого объектно-ориентированного языка программирования. Иногда такая последовательность вызовов может удивить и, что гораздо хуже, даже «завалить» программу в самый неожиданный момент.

Итак, рассмотрим класс MyLogger:

class MyLogger
{
    static MyLogger innerInstance = new MyLogger();

    static MyLogger()
    {
        Console.WriteLine("Static Logger Constructor");
    }

    private MyLogger()
    {
        Console.WriteLine("Instance Logger Constructor");
    }

    public static MyLogger Instance { get { return innerInstance; } }
}

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

Посмотрим, что есть в нашем классе MyLogger:

  1. Задан статический конструктор
  2. Есть приватный конструктор без параметров
  3. Определена закрытая статическая переменная innerInstance
  4. И есть открытое статическое свойство Instance для общения с внешним миром

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

Снаружи класса (без использования ухищрений типа reflection) мы можем использовать только открытое статическое свойство Instance, которое можем вызвать так:

class Program
{
    public static void Main()
    {
        var logger = MyLogger.Instance;
    }
}

Что выведет эта программа?
Все мы знаем, что статический конструктор вызывается перед доступом к любому члену класса (за исключением констант). При этом запускается он единственный раз в рамках application domain.

В нашем случае мы обращаемся к члену класса — свойству Instance, что должно вызвать сначала запуск статического конструктора, а потом вызовет конструктор экземпляра класса. Т.е. программа выведет:

Static Logger Constructor
Instance Logger Constructor


Однако после запуска программы мы получим на консоли:

Instance Logger Constructor
Static Logger Constructor


Как так? Инстанс конструктор отработал раньше статического конструктора?!?
Ответ: Да!

И вот почему.

В стандарте ECMA-334 языка C# на счет статических классов указано следующее:

17.4.5.1: «If a static constructor (§17.11) exists in the class, execution of the static field initializers occurs immediately prior to executing that static constructor.

17.11: … If a class contains any static fields with initializers, those initializers are executed in textual order immediately prior to executing the static constructor


(Что в вольном переводе значит: если в классе есть статический конструктор, то запуск инициализации статических полей происходит непосредственно ПЕРЕД запуском статического конструктора.

Если класс содержит какие-либо статические поля с инициализаторами, то такие инициализаторы запускаются в порядке следования в тексте программы непосредственно ПЕРЕД запуском статического конструктора.)

В нашем случае статическое поле innerInstance объявлено вместе с инициализатором, в качестве которого выступает конструктор экземпляра класса. Согласно стандарту ECMA инициализатор должен быть вызван ПЕРЕД вызовом статического конструктора. Что и происходит в нашей программе: конструктор экземпляра, являясь инициализатором статического поля, вызывается ДО статического конструктора. Согласитесь, довольно неожиданно.

Обратим внимание, что это верно только для инициализаторов статических полей. В общем случае статический конструктор вызывается ПЕРЕД вызовом конструктора экземпляра класса.

Как, например, тут:

class MyLogger
{
    static MyLogger()
    {
        Console.WriteLine("Static Logger Constructor");
    }

    public MyLogger()
    {
        Console.WriteLine("Instance Logger Constructor");
    }
}

class Program
{
    public static void Main()
    {
        var logger = new MyLogger();
    }
}

И программа ожидаемо выведет на консоль:

Static Logger Constructor
Instance Logger Constructor


image

Пример 3


Программистам часто приходится писать вспомогательные функции (утилиты, хелперы и т.д.), чтобы облегчать себе жизнь. Обычно такие функции довольно просты и часто занимают всего несколько строк кода. Но споткнуться можно даже на ровном месте.

Пусть нам необходимо реализовать такую функцию, которая проверяет число на нечетность (т.е. что число не делится на 2 без остатка).

Реализация может выглядеть так:

static bool isOddNumber(int i)
{
    return (i % 2 == 1);
}

На первый взгляд все хорошо и, например, для чисел 5,7 и 11 мы ожидаемо получаем True.

А что вернет функция isOddNumber(-5)?
-5 нечетное число, но в качестве ответа нашей функции мы получим False!
Разберемся, в чем причина.

Согласно MSDN про оператор остатка от деления % написано следующее:
«Для целочисленных операндов результатом a % b является значение, произведенное a — (a / b) * b»
В нашем случае для a=-5, b=2 мы получаем:
-5 % 2 = (-5) — ((-5) / 2) * 2 = -5 + 4 = -1
Но -1 всегда не равно 1, что объясняет наш результат False.

Оператор % чувствителен к знаку операндов. Поэтому, чтобы не получать таких «сюрпризов», лучше результат сравнивать с нулем, у которого нет знака:

static bool isOddNumber(int i)
{
    return (i % 2 != 0);
}

Или завести отдельную функцию проверки на четность и реализовать логику через нее:

static bool isEvenNumber(int i)
{
    return (i % 2 == 0);
}

static bool isOddNumber(int i)
{
    return !isEvenNumber(i);
}


Пример 4


Все, кто программировал на C#, наверняка встречался с LINQ, на котором так удобно работать с коллекциями, создавая запросы, фильтруя и агрегируя данные…

Мы не будем заглядывать под капот LINQ. Возможно, сделаем это в другой раз.

А пока рассмотрим небольшой пример:

int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };

int summResult = 0;
var selectedData = dataArray.Select(
    x =>
    {
        summResult += x;
        return x;
    });

Console.WriteLine(summResult);

Что выведет этот код?
Мы получим на экране значение переменной summResult, которое равно начальному значению, т.е. 0.

Почему так произошло?

А потому, что определение LINQ запроса и запуск этого запроса – это две операции, которые выполняются отдельно. Таким образом, определение запроса еще не означает его запуск/выполнение.

Переменная summResult используется внутри анонимного делегата в методе Select: последовательно перебираются элементы массива dataArray и прибавляются к переменной summResult.

Можно предположить, что наш код напечатает сумму элементов массива dataArray. Но LINQ работает не так.

Рассмотрим переменную selectedData. Ключевое слово var – это «синтаксический сахар», который во многих случаях сокращает размер кода программы и улучшает ее читабельность. А настоящий тип переменной selectedData реализует интерфейс IEnumerable. Т.е. наш код выглядит так:

    IEnumerable<int> selectedData = dataArray.Select(
    x =>
    {
        summResult += x;
        return x;
    }); 

Здесь мы определяем запрос (Query), но сам запрос не запускаем. Схожим образом можно работать с базой данных, задавая SQL запрос в виде строки, но для получения результата обращаясь к базе данных и запуская этот запрос в явном виде.

То есть пока мы только задали запрос, но не запустили его. Вот почему значение переменной summResult осталось без изменений. А запустить запрос можно, например, с помощью методов ToArray, ToList или ToDictionary:

int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };

int summResult = 0;

// определяем запрос и сохраняем его в переменной selectedData
IEnumerable<int> selectedData = dataArray.Select(
    x =>
    {
        summResult += x;
        return x;
    });

// запускаем запрос selectedData
selectedData.ToArray();

// печатаем значение переменной summResult
Console.WriteLine(summResult);

Этот код уже выведет на экран значение переменной summResult, равное сумме всех элементов массива dataArray, равное 15.

С этим разобрались. А что тогда выведет на экран эта программа?

int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; //1
var summResult = dataArray.Sum() + dataArray.Skip(3).Take(2).Sum(); //2
var groupedData = dataArray.GroupBy(x => x).Select(  //3
    x =>
    {
        summResult += x.Key;
        return x.Key;
    });

Console.WriteLine(summResult); //4

Переменная groupedData (строка 3) на самом деле реализует интерфейс IEnumerable и по сути определяет запрос к источнику данных dataArray. А это значит, что для работы анонимного делегата, который изменяет значение переменной summResult, этот запрос должен быть запущен явно. Но такого запуска в нашей программе нет. Поэтому значение переменной summResult будет изменено только в строке 2, а все остальное мы можем не рассматривать в наших вычислениях.

Тогда нетрудно посчитать значение переменной summResult, которое равно, соответственно, 15 + 7, т.е. 22.

Пример 5


Скажем сразу — этот пример мы не рассматриваем на наших лекциях в Академии, но иногда обсуждаем во время кофе-брейков скорее как анекдот.

Несмотря на то, что он вряд ли является показательным с точки зрения определения уровня разработчика, этот пример встречался нам в нескольких различных тестах. Возможно, его используют за универсальность, потому что он отрабатывает одинаково как на C и C++, так и на C# и Java.

Итак, пусть есть строка кода:

int i = (int)+(char)-(int)+(long)-1;

Чему будет равно значение переменной i?
Ответ: 1

Можно подумать, что здесь используется численная арифметика над размерами каждого типа в байтах, поскольку для преобразования типов тут довольно неожиданно встречаются знаки «+» и «-».

Известно, что в C# тип integer имеет размер 4 байта, long – 8, char – 2.

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

int i = (4)+(2)-(4)+(8)-1;

Однако это не так. А чтобы сбить с толку и направить по такому ложному рассуждению, пример может быть изменен, например, так:

int i = (int)+(char)-(int)+(long)-sizeof(int);

Знаки «+» и «-» используются в этом примере не как бинарные арифметические операции, а как унарные операторы. Тогда наша строка кода является лишь последовательностью явных преобразований типов, перемешанных с вызовами унарных операций, которую можно записать так:

        int i =
        (int)( // call explicit operator int(char), i.e. char to int
          +( // call unary operator +
            (char)( // call explicit operator char(int), i.e. int to char
              -( // call unary operator -
                (int)( // call explicit operator int(long), i.e. long to int
                  +( // call unary operator +
                    (long)( // call explicit operator long(int), i.e. int to long
                      -1
                    )
                  )
                )
              )
            )
          )
        );


image

Заинтересовало Обучение в Veeam Academy?


Сейчас идет набор на весенний интенсив по C# в Санкт-Петербурге, и мы приглашаем всех желающих пройти онлайн-тестирование на сайте Veeam Academy.

Курс стартует 18 февраля 2019 г., продлится до середины мая и будет, как всегда, совершенно бесплатным. Регистрация для всех, кто хочет пройти входное тестирование, уже доступна на сайте Академии: academy.veeam.ru

image
Теги:
Хабы:
Всего голосов 41: ↑32 и ↓9+23
Комментарии46

Публикации

Информация

Сайт
veeam.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Швейцария

Истории