Головоломки на C# (Ответы)

Введение

Итак, в предыдущем посте мною были приведены 6 вариантов необычного поведения языковых конструкций на C#. В этом посте я постараюсь дать более-менее внятное объяснение данных особенностей происходящего.

Ответы

Далее я продублирую приведенные фрагменты кода, а так же дополню их описанием нюансов

  1. Перегрузка

    Как вы думаете что выведет данный код и почему?

    using System;
    
    class Base
    {
        public virtual void Foo(int x)
        {
            Console.WriteLine ("Base.Foo(int)");
        }
    }
    
    class Derived : Base
    {
        public override void Foo(int x)
        {
            Console.WriteLine ("Derived.Foo(int)");
        }
        
        public void Foo(object o)
        {
            Console.WriteLine ("Derived.Foo(object)");
        }
    }
    
    class Test
    {
        static void Main()
        {
            Derived d = new Derived();
            int i = 10;
            d.Foo(i);
        }
    }

    Результат: Derived.Foo(object).Мы получили его таким, т.к. при выборе подходящего перегруженного метода в наследуемом типе компилятор игнорирует определенный в базовом типе метод, даже если он был переопределен в наследуемом!

  2. Порядок! Порядок!

    В каком порядке напечатаются строки на экране?

    using System;
    
    class Foo
    {
        static Foo()
        {
            Console.WriteLine ("Foo");
        }
    }
    
    class Bar
    {
        static int i = Init();
        
        static int Init()
        {
            Console.WriteLine("Bar");
            return 0;
        }
    }
    
    class Test
    {
        static void Main()
        {
            Foo f = new Foo();
            Bar b = new Bar();
        }
    } 

    Результат: В варианте Джона он получает следующую последовательность: “Bar, Foo”, однако при воспроизведении на своей машине у меня получилось: “Foo, Bar”. Я думаю, что причина различий лежит в разнице версий исполняющей среды (CLR, я исполнял код на версии 2.0.50727.3053). Он объясняет свои результаты тем, что у Foo конструктор статический, следовательно он не будет выполнен до точки первой инициализации этого класса. У Bar же нет статического конструктора, что позволяет CLR инициализировать его раньше. Однако, нет гарантии, что Bar будет напечатан вовсе, так как в нашем примере у CLR в принципе нет надобности инициализировать это статическое поле. Более подробно все эти нюансы описаны здесь!

  3. “Глупая” арифметика

    Компьютеры призваны помочь нам с арифметическими вычислениями, но почему тогда это выражение выводит false?

    double d1 = 1.000001;
    double d2 = 0.000001;
    Console.WriteLine((d1-d2)==1.0); 

    А такой вариант?

    double sum = 0;
    for (int i =0;i<10 ;i++ )
    {
      sum+=0.1;    
    }
    Console.WriteLine(sum==1.0); 

    Результат: Все значения в данных примерах хранятся в двоичном виде, хотя 1.0 может хранится как точное значение, 1.000001 будет храниться примерно как 1.0000009999999999177333620536956004798412322998046875, и 0.000001 так же будет сохранено в виде 0.000000999999999999999954748111825886258685613938723690807819366455078125. В связи с этим нельзя утверждать о равенстве в предыдущих примерах. Почитать подробнее о числах с плавающей точкой.

    На заметку: В связи с указанными выше особенностями работы с типами с плавающей точкой, однозначно рекомендуют не использовать их при работе с денежными единицами, в данном случае рекомендуют определять собственные денежные типы (так называемые типы объект-значение [Object-Value]) с собственной логикой округления и прочих арифметических операций, а так же межвалютных операций.

  4. Печать, печать, печать

    Ниже приведен код, использующий анонимные методы, каков будет результат его исполнения?

    using System;
    using System.Collections.Generic;
    
    class Test
    {
        delegate void Printer();
        
        static void Main()
        {
            List<Printer> printers = new List<Printer>();
            for (int i=0; i < 10; i++)
            {
                printers.Add(delegate { Console.WriteLine(i); });
            }
            
            foreach (Printer printer in printers)
            {
                printer();
            }
        }
    } 

    Результат: О эта радость от фиксированных переменных. В данном примере всего одна переменная и её значение изменяется при каждой итерации в цикле. Анонимные методы фиксируют переменную, а не её значение, следовательно в результате мы получим десть раз число 10 на выводе!

  5. Ничего странного с компилятором тут не случилось

    Этот код скомпилится? Как это? Что бы это значило?

    using System;
    
    class Test
    {
        enum Foo { Bar, Baz };
        
        static void Main()
        {
            Foo f = 0.0;
            Console.WriteLine(f);
        }
    }

    Результат: Это не должно компилироваться, однако это происходит. Это не должно копилироваться, в связи с тем, что только значение 0 может быть конвертированно в значение по умолчанию для enum. В примере же значение 0.0 имеет место быть корректным в связи с небольшой недоработкой компилятора. В результате будет напечатано Bar, т.к. 0 будет значением Foo.

    using System;
    
    class Test
    {
        enum Foo { Bar, Baz };
        
        const int One = 1;
        const int Une = 1;
        
        static void Main()
        {
            Foo f = One-Une;
            Console.WriteLine(f);
        }
    } 

    Результат: Данный пример не будет компилироваться под C#2.0, но прекрасно компилируется под C#3.0. Это известная особенность, объяснить это можно тем, что оптимизированный алгоритм выполняет вычисления значения One-Une раньше, а следовательно перечисление даёт своё значение по-умолчанию 0.

  6. Вывод типа

    Какой же вариант будет выведен на экран?

    using System;
    
    class Test
    {
        static void Main()
        {
            Foo("Hello");
        }
        
        static void Foo(object x)
        {
            Console.WriteLine("object");
        }
        
        static void Foo<T>(params T[] x)
        {
            Console.WriteLine("params T[]");
        }
    } 

    Результат: params T[] будет напечатан. Но почему же компилятор выбирает создание массива, хотя в этом нет надобности? Этому есть две причины. Во-первых при выборе перегруженных методов T будет рассматриваться как System.String. Ничего страшного пока в этом нет.

    Однако при выборе “лучшего” метода среди параметров string x and params string[],x формально будет приоритетнее. Однако с нашей точки зрения эффективности выбор между object x и params string[] x падёт на метод с params string[] x в связи с тем, что он не требует преобразований типов.

Материалы

 

  1. Подготовлено по материалам http://www.yoda.arachsys.com/csharp/teasers-answers.html
  2. А так же http://martinfowler.com/
Advertisements

One Comment on “Головоломки на C# (Ответы)”

  1. Oleg:

    По первой задачи, если убрать слово override, то выведется Derived.Foo(int).
    Сидел, изучал IL код, но так и не смог понять, почему при наличии виртуальных методов применяется упаковка:
    IL_000c: box [mscorlib]System.Int32


Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s