Предлагаю Вам ряд вопросов по C# и .NET в целом, которые могут пригодиться для проведения собеседования или просто помогут лучше понять, как работает платформа .NET. Здесь не будет обычных вопросов о том, чем отличаются ссылочные типы от значимых и тп. Я постарался выбрать самые интересные, над которыми стоит задуматься.

  1. Известно, что при размещении объекта ссылочного типа в куче у него есть указатель на объект-тип (область памяти, содержащую статические поля и реализацию статических методов). Этот объект-тип содержит индекс блока синхронизации и еще один указатель на объект-тип. Зачем он нужен и куда указывает?

    Ответ
    В CLR каждый объект в куче имеет указатель на объект-тип. Это нужно для того, чтобы, например, найти значения статических полей и реализацию статических методов для экземпляра типа. Но объект-тип, на который ссылается экземпляр типа так же имеет ссылку на объект-тип и является «экземпляром» для объекта-типа System.Type, объект-тип для которого создается CLR при запуске.

    На этой схеме объект Manager ссылается на объект-тип Manager, указатель на объект-тип которого ссылается на объект-тип System.Type.

  2. Можно ли объявить делегат не только внутри класса, но и в глобальной области видимости? Почему?

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

    internal class Feedback : System.MulticastDelegate {
       // Конструктор
       public Feedback(Object object, IntPtr method);
       // Метод, прототип которого задан в исходном тексте
       public virtual void Invoke(Int32 value);
       // Методы, обеспечивающие асинхронный обратный вызов
       public virtual IAsyncResult BeginInvoke(Int32 value,
       AsyncCallback callback, Object object);
       public virtual void EndInvoke(IAsyncResult result);
    }
    

    Еще интересный вопрос — почему конструктор класса делегата содержит два параметра, а в коде мы просто передаем указатель на метод (внутрений для CLR, по которому этот метод она найдет)?

    delegate void Test(int value);
    void A(int v) 
    { 
       Console.WriteLine(v); 
    }  
    void TestDelegate()
    {
       var t = new Test(A);
       t(1);
    }
    

    Все просто — потому что компилятор при создании делегата сам подставляет в конструктор значение параметра оbject. Если метод, которым инициализируется делегат статический, то передается null. Иначе передается объект экземпляра класса, которому принадлежит метод. В этом случае состояние этого объекта может быть изменено через ключевое слово this внутри метода.

  3. Простой вопрос — что выведет на экран метод Test и почему?

    delegate int GetValue();
    int Value1() { return 1; }
    int Value2() { return 2; }
    void Test()
    {
       var v1 = new GetValue(Value1);
       var v2 = new GetValue(Value2);
       var chain = v1;
       chain += v2;
       Console.WriteLine(chain());
    }
    

    Ответ
    Выведет 2. При помещении делегатов в цепочку у делегата chain заполняется внутреннее поле, которое представляет из себя массив делегатов (в случае, если количество больше одного, иначе просто хранится ссылка на метод). Все делегаты выполняются последовательно. Возвращается значение последнего, остальные не учитываются.

  4. Объясните, каким образом локальные переменные pass1 и pass2 из метода Test передаются в лямбда-выражение, если WaitCallback принимает лишь один параметр(и в данном случае ссылка на него равна null).

    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                var p = new Program();
                p.Test();
                Console.ReadKey();
            }
    
            void Test()
            {
                int pass1 = 5;
                object pass2 = "Passing test";
                ThreadPool.QueueUserWorkItem((obj) => 
                {
                    Console.WriteLine(pass1);
                    Console.WriteLine(pass2);    
                });            
            }
        }
    }
    

    Ответ
    Для того, чтобы в этом разобраться, открываем сборку в ildasm.
    Можете убедиться, что в этом случае лямбда выражение — это не метод, а целый класс!

    .method private hidebysig instance void  Test() cil managed
    {
      // Размер кода:       44 (0x2c)
      .maxstack  2
      .locals init ([0] class ConsoleApplication1.Program/'<>c__DisplayClass1_0' 'CS$<>8__locals0')
      IL_0000:  newobj     instance void ConsoleApplication1.Program/'<>c__DisplayClass1_0'::.ctor()
      IL_0005:  stloc.0
      IL_0006:  nop
      IL_0007:  ldloc.0
      IL_0008:  ldc.i4.5
      IL_0009:  stfld      int32 ConsoleApplication1.Program/'<>c__DisplayClass1_0'::pass1
      IL_000e:  ldloc.0
      IL_000f:  ldstr      "Passing test"
      IL_0014:  stfld      object ConsoleApplication1.Program/'<>c__DisplayClass1_0'::pass2
      IL_0019:  ldloc.0
    // вот создается этот класс!
      IL_001a:  ldftn      instance void   ConsoleApplication1.Program/'<>c__DisplayClass1_0'::'<Test>b__0'(object)
      IL_0020:  newobj     instance void [mscorlib]System.Threading.WaitCallback::.ctor(object,
                                                                                        native int)
      IL_0025:  call       bool [mscorlib]System.Threading.ThreadPool::QueueUserWorkItem(class [mscorlib]System.Threading.WaitCallback)
      IL_002a:  pop
      IL_002b:  ret
    } // end of method Program::Test
    

    А вот описание самого класса и он содержит обсуждаемый метод:

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

  5. Что выведет на экран следующий код?

    int a = -5;
    Console.WriteLine(~a);
    

    Ответ
    Выведет 4. Оператор ~ производит побитовую реверсию.

    Console.WriteLine("{0:x8}, {1:x8}", -5, ~(-5));
    // выведет fffffffb, 00000004
    

    Причем для значения 5 выведет -6.

  6. Обычно управлять в ручную уборкой мусора не рекоммендуется. Почему? Приведите пример, когда вызов метода GC.Collect() имеет смысл.

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

  7. Бонус с собеседования: есть метод rand2, выдающий 0 или 1 с одинаковой вероятностью. Написать метод rand3, использующий метод rand2, выдающий 0,1,2 с одинаковой вероятностью.

    Ответ
    // первое решение
    int rand3()
    {
        int x, y;
    
        do {
            x = rand2();
            y = rand2();
        } while (x == 0 && y == 1);
    
        return x + y;
    }
    // второе решение
    int rand3()
    {
        int r = 2 * rand2() + rand2();     
        if (r < 3)
            return r; 
        return rand3();
    }
    


Любая критика приветствуется. Вопросы есть еще по другим темам, если интересно.
Поделиться с друзьями
-->

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


  1. Aler
    15.06.2017 12:14

    Спасибо за статью, полезно. Я бы результат ранд2 принял бы за бинарный рандом и ранд3 реализовал бы через сдвиги:

    int Rand3() {
        return rand2() | (rand2() << 1);
    }
    


    1. Aler
      15.06.2017 12:15
      +3

      Хотя нет, извините, нельзя конечно же. Будет еще нежелательное 3 при двух единицах


    1. kenoma
      15.06.2017 13:11
      +2

      *удалил неправильный ответ


  1. zawodskoj
    15.06.2017 14:41

    static int rand3() => 
        (int) ((rand2() + rand2() * 2 + rand2() * 4 + rand2() * 8 + rand2() * 16 + 
                rand2() * 32 + rand2() * 64) / 128.0 * 3);
    


    1. kenoma
      15.06.2017 17:51
      +1

      Это же аппроксимированное решение, вероятности не будут одинаковыми.


  1. master65
    15.06.2017 16:34
    +1

    // первое решение
    int rand3()
    {
    int x, y;

    do {
    x = rand2();
    y = rand2();
    } while (x == 0 && y == 1);

    return x + y;
    }

    Но это всегда вернет 1


    1. sir_Maverick
      15.06.2017 16:53

      не, 25% — 0 и 0, 25% — 1 и 1, 25% — 1 и 0 и 25% — 0 и 1. While как раз откинет одну пару 0 и 1 для равновероятного появление 0 1 и 2.


    1. build_your_web
      16.06.2017 14:41

      del


  1. sir_Maverick
    15.06.2017 16:40

    return (rand2() + rand2()+ rand2()+ rand2()+ rand2())%3
    


    1. sdev
      15.06.2017 18:16
      +1

      Увы нет. В скобках 2^5 вариантов, т.е. 32 различных варианта. Это не кратно 3.


    1. shrstk
      15.06.2017 18:18

      Тут распределение не будет равномерным — единичка выпадает на примерно 3% реже, чем 0 и 2


      1. sir_Maverick
        16.06.2017 01:15
        -1

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


        1. mayorovp
          16.06.2017 15:18
          +1

          С чего бы? Ваши вероятности для того, что в скобках — такие:


          1/32, 5/32, 10/32, 10/32, 5/32, 1/32


          По модулю 3 получаются более равномерные, но все еще не равные вероятности:


          11/32, 10/32, 11/32


          1. sir_Maverick
            16.06.2017 15:23

            Можете рассказать, как вы это считали? Какие формулы и правила использовали? На самом деле интересно, т.к. не знаком с тервером практически никак.


            1. mayorovp
              16.06.2017 20:05
              +1

              Получить 0 или 5 в скобках можно только 1 способом — все слагаемые должны быть 0 или 1 соответственно.


              Получить 1 или 4 можно 5 способами — одно из слагаемых должно отличаться от остальных.


              Получить 2 или 3 можно 10 способами — потому что 5!/2!3!, подробно объяснять лень.


  1. Chamie
    15.06.2017 16:56

    del


  1. EreminD
    15.06.2017 17:05

    int rand3()
            {
                switch (rand2().ToString() + rand2().ToString()) {
                    case "00":
                        return 0;
                    break;
                    case "01":
                        return 1;
                    break;
                    case "10":
                        return 2;
                    break;
                    case "11":
                        return rand3();
                    break;
                    default:
                        return rand3();
                }
            }
    


    1. Zam_dev
      15.06.2017 18:16

      int rand3()
              {
                  int v= (rand2() + rand2()) {
                  return v>2? rand3(): v  
              }
      


      1. sdev
        15.06.2017 18:22

        rand2() + rand2() никогда не больше 2


        1. Zam_dev
          15.06.2017 22:02

          rand2() + rand2() +rand2() так уж и быть)


          1. sdev
            16.06.2017 10:46

            опять мимо


        1. Zam_dev
          15.06.2017 22:13

          Судя по тому, что самый популярный ответ именно по Random, можно предположить, что вопросы, либо скучнейшие или… мы что то не догоняем, еще есть вариант — зачем мне это??)


          1. sdev
            16.06.2017 10:57

            Я думал что есть какой-то красиывый вариант, но, видимо, нет его. Нужно пергениерировать и писать циклы и условия.


  1. sdev
    15.06.2017 17:38
    -1

    int rand3()
    {
    return rand2()+rand2();
    }


    1. sdev
      15.06.2017 17:48

      Вижу что ошибся, 2 выпадет чаще, чем 1 или 0


      1. vkushnir
        15.06.2017 20:02
        +1

        1 же…
        Рассмотрим варианты появления:
        0: при 0+0
        1: при 1+0 или 0+1
        2: при 1+1

        Вероятности:
        0: 1/2*1/2 = 1/4
        1: 1/2*1/2 + 1/2*1/2 = 2/4 = 1/2
        2: 1/2*1/2 = 1/4

        Следовательно вероятность появления 1 — 50%, 0 и 2 — 25%…


        1. sdev
          16.06.2017 10:46

          Да 1.


  1. shai_hulud
    15.06.2017 17:51
    +3

    О очередной «i++ + i++» с собеседований запостили.

    1) детали имплементации рантайма .NET, есть еще Mono, еще есть Mono под LLVM, есть еще отнсительно новый RyuJIT
    2) О май гад, делегат это тип, в рот мне тапки. В каждой книге по С# об этом говорят.
    3) Дети, а теперь давайте поработаем интерпретатором
    4) З — Замыкания. Надеюсь автор знал правильный ответ.
    5) Хрен проссышь что там тильда а не минус, пятерочка за крипто-операторы вроде бинарного комплемента (~) и отрицания (!).
    6) GC.Collect() нет смысла вызывать никогда

    Тогда вопрос автору, как сделать каст без боксинга и создания новых объектов в выражении:

    // не меняя сигнатуру естественно
    public static int CastToInt<T>(T value)
    {
        return (?)value;
    }
    


    1. AntonioGrande
      15.06.2017 18:23

      А расскажите.


      1. shai_hulud
        15.06.2017 18:27

        Ближе к вечеру кину правильный ответ. Зачем портить удовольствие тому кто попытается его найти.


        1. AntonioGrande
          15.06.2017 18:42

          Вообще для того, чтобы писать хороший код, нужно понимать как работает фреймворк и среда исполнения. Это особенно важно, если вы hft трейдер, например. Пишете на с++, разбирайтесь как работает компилятор. Пишете под mono, читайте про mono. Это позволит понять, как ваш код оптимизировать. А подход «оно и так работает» мне лично непонятен. В конце концов вы же и заинтересованы в том, чтобы ваше приложение работало быстро и стабильно.


          1. shai_hulud
            15.06.2017 19:00
            +1

            >Вообще для того, чтобы писать хороший код, нужно понимать как работает фреймворк и среда исполнения.
            Согласен. Но примеры в статье не про хороший код.
            3) комбинация делегатов и использования результата вызова комбинации делегатов
            4) замыкания, неявное создание объектов в куче
            5) использования редкого оператора, где его визуально можно спутать с другим оператором (-)
            6) Явные вызок GC.Collect() плохо пахнет даже с отговорками


          1. vkushnir
            15.06.2017 21:09

            Как человек, который уже третий год работает в финансовой сфере (не подвального уровня, а в компаниях международного уровня) могу с увереностью сказать, что гавнокода тут не меньше, чем в проектах других представителей энтерпрайз сектора, а может и больше. И поверьте мне на слово, трейдеров это не так сильно волнует, к сожалению… Самого это жутко бесит, но это реалии. Для обработки же большого кол-ва ордеров сейчас делается упор на количестве железа и масштабируемых системах, а не на «выжимке» из железа максимума… Им проще докупить сервер, чем платить специалисту за знание тонкостей работы железа, ОС и прочего. Касательно статьи, спасибо, большая часть и вправду может быть полезной, но не всё:

            1) Возможно, интересно знать, но на практике есть этим знаниям применение? Не думаю… Меня как-то на собеседовании спросили, какой бит используется GC для маркировки обьекта после прохода по нему (при анализе, что нужно собрать). На кой, спрашивается, это нужно знать? Возможно, однажды, будет какой-то кейс где это может как-то пригодиться, но на такие случаи достаточно это один раз загуглить и забыть, а не держать такие знания в памяти…
            4) Замыкание же. Обьезженая тема, как по мне. Главное понимать как это работает на практике, я думаю, а превращается ли это в отдельный клас в нутрях, важно ли это? Если да, то как часто?

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


    1. vkushnir
      15.06.2017 20:16

      public static int CastToInt(T value)
      {
      return (int)(dynamic)value;
      }

      Скомпилируется и скастит, скажем, double в int. Для остального (типа стринга) пошлёт с эксешпеном :)


      1. darkdaskin
        15.06.2017 21:33
        +2

        Боксинг всё равно будет. Компилятор генерирует такой код:


        public static int CastToInt<T>(T v)
        {
            if (Test.<>o__0<T>.<>p__0 == null)
            {
                Test.<>o__0<T>.<>p__0 = CallSite<Func<CallSite, object, int>>.Create(Binder.Convert(CSharpBinderFlags.ConvertExplicit, typeof(int), typeof(Test)));
            }
            return Test.<>o__0<T>.<>p__0.Target(Test.<>o__0<T>.<>p__0, v);
        }

        Где Target имеет тип Func<CallSite, object, int>. А при первом вызове к тому же будут созданы новые объекты.


    1. darkdaskin
      15.06.2017 21:54
      +3

      Аналог (int)(object)value без боксинга будет таким:


      public static int CastToInt2<T>(T v)
      {
           return __refvalue(__makeref(v), int);
      }

      Оба варианта работают только когда typeof(T) == typeof(int). Для непрямых приведений (например, из double к int) можно написать что-то вроде:


      public static int CastToInt2<T>(T v)
      {
          if (typeof(T) == typeof(int))
              return __refvalue(__makeref(v), int);
          else if (typeof(T) == typeof(double))
              return (int) __refvalue(__makeref(v), double);
          // Similar conditions vor all possible casts
          else
              throw new InvalidCastException();
      }


      1. shai_hulud
        15.06.2017 22:57

        Да, это правильный вариант "__refvalue(__makeref(v), int)".
        Других вариантов без лишних аллокаций нет.


        1. darkdaskin
          16.06.2017 14:28
          +2

          На самом деле (int)(object)value тоже может работать без выделения памяти, в зависимости от версии JIT. У меня получается такой код в x86 Release:
          Boxing elimination JIT x86 Release


          Для сравнения (object)value:
          Boxing JIT x86 Release


          А вот вариант с __refvalue(__makeref(v), int), тут кода уже больше и тоже есть внешний вызов:
          Refvalue JIT x86 Release


          Этот же код в x64 Release, (int)(object)value опять побеждает:
          Boxing elimination JIT x64 Release
          Boxing JIT x64 Release
          Refvalue JIT x64 Release


          Не зря говорят, что преждевременная оптимизация — корень всех зол.


          1. shai_hulud
            16.06.2017 16:11
            +1

            Это сравнение того кода который сгенерит JIT компилятор.
            Ему уже известно что вместо дженерика будет int, и кастить int в int он не будет.
            Но, этот компилятор не будет, или в этой версии не будет. Это надежда на оптимизации компилятора. Не спорю что полезно о них знать, но оптимизации могут и не случиться, и приложение начнет засирать память.

            Хотя наблюдение довольно интересное.


  1. sdev
    15.06.2017 18:06

    уладено. опять неправильно решил последнюю


  1. Spiceman
    16.06.2017 09:49
    +1

    Бонус так решил:

     static class A
     {
         static Random r = new Random();
         static int c = 0;
         public static int rand3()
         {
             c = (c + 1) % 3;
             while (r.Next(2) == 0)
                 c++;
             return c % 3;
         }
     }
    

    Проверка:
    > Enumerable.Range(0, 10000).Select(_ => A.rand3()).GroupBy(_ => _).Select(g => g.Count()).ToArray()
    int[3] { 3365, 3300, 3335 }


    1. sdev
      16.06.2017 10:54

      Вас не смущает, то что весь цикл while можно убрать и ваш тест не изменится?


      1. Spiceman
        16.06.2017 18:50

        Точнее заменить while на if. Спасибо, сразу не сообразил.


        1. sdev
          16.06.2017 19:16
          +1

          Уж лучше оставить while. Здесь проблема в том, что имея такой генератор случайных чисел, можно с какой-то вероятностью, большей чем 0.3 предсказывать значение следующего числа.


  1. pelhu
    16.06.2017 10:03

    Спасибо за статью, полезно.
    Предлогаю свое решения последнего вопроса.
    int rand3() {
    int r = rand2();
    return r == 0? r: r + rand2();
    }


    1. sdev
      16.06.2017 10:55

      0 будет выпадать с вероятностью 0.5, 1 и 2 по 0.25. Совсем не то, что просили


  1. GLeBaTi
    16.06.2017 10:03
    +1

    Вопрос по третьему пункту. Зачем нужен MulticastDelegate, если у обычного делегата уже есть Cobine(+=)?


    1. AntonioGrande
      16.06.2017 10:08

      Цитата:

      Любые типы делегатов — это потомки класса MulticastDelegate, от которого
      они наследуют все поля, свойства и методы.

      Класс System.MulticastDelegate является производным от класса System.Delegate,
      который, в свою очередь, наследует от класса System.Object. Два класса делегатов
      появились исторически, в то время как в FCL предполагался только один.


      1. GLeBaTi
        16.06.2017 10:26

        Спасибо