15 июня в 11:50

Интересные вопросы на знание C# и механизмов .NET

C#*, .NET*
Предлагаю Вам ряд вопросов по 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();
    }
    


Любая критика приветствуется. Вопросы есть еще по другим темам, если интересно.
Anton @AntonioGrande
карма
12,0
рейтинг 0,0
Software Developer
Похожие публикации
Самое читаемое Разработка

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

  • 0
    Спасибо за статью, полезно. Я бы результат ранд2 принял бы за бинарный рандом и ранд3 реализовал бы через сдвиги:
    int Rand3() {
        return rand2() | (rand2() << 1);
    }
    
    • +3
      Хотя нет, извините, нельзя конечно же. Будет еще нежелательное 3 при двух единицах
    • +2

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

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

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

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

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

    return x + y;
    }

    Но это всегда вернет 1
    • 0
      не, 25% — 0 и 0, 25% — 1 и 1, 25% — 1 и 0 и 25% — 0 и 1. While как раз откинет одну пару 0 и 1 для равновероятного появление 0 1 и 2.
    • 0
      del
  • 0
    return (rand2() + rand2()+ rand2()+ rand2()+ rand2())%3
    
    • +1
      Увы нет. В скобках 2^5 вариантов, т.е. 32 различных варианта. Это не кратно 3.
    • 0
      Тут распределение не будет равномерным — единичка выпадает на примерно 3% реже, чем 0 и 2
      • –1
        Разве вероятность появления в скобках числа от нуля до пяти не будет одинакова для каждого числа?
        • +1

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


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


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


          11/32, 10/32, 11/32

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

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


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


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

  • 0
    del
  • 0
    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();
                }
            }
    
    • 0
      int rand3()
              {
                  int v= (rand2() + rand2()) {
                  return v>2? rand3(): v  
              }
      
      • 0
        rand2() + rand2() никогда не больше 2
        • 0
          rand2() + rand2() +rand2() так уж и быть)
          • 0
            опять мимо
        • 0
          Судя по тому, что самый популярный ответ именно по Random, можно предположить, что вопросы, либо скучнейшие или… мы что то не догоняем, еще есть вариант — зачем мне это??)
          • 0
            Я думал что есть какой-то красиывый вариант, но, видимо, нет его. Нужно пергениерировать и писать циклы и условия.
  • –1
    int rand3()
    {
    return rand2()+rand2();
    }
    • 0
      Вижу что ошибся, 2 выпадет чаще, чем 1 или 0
      • +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%…
        • 0
          Да 1.
  • +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;
    }
    

    • 0
      А расскажите.
      • 0
        Ближе к вечеру кину правильный ответ. Зачем портить удовольствие тому кто попытается его найти.
        • 0
          Вообще для того, чтобы писать хороший код, нужно понимать как работает фреймворк и среда исполнения. Это особенно важно, если вы hft трейдер, например. Пишете на с++, разбирайтесь как работает компилятор. Пишете под mono, читайте про mono. Это позволит понять, как ваш код оптимизировать. А подход «оно и так работает» мне лично непонятен. В конце концов вы же и заинтересованы в том, чтобы ваше приложение работало быстро и стабильно.
          • +1
            >Вообще для того, чтобы писать хороший код, нужно понимать как работает фреймворк и среда исполнения.
            Согласен. Но примеры в статье не про хороший код.
            3) комбинация делегатов и использования результата вызова комбинации делегатов
            4) замыкания, неявное создание объектов в куче
            5) использования редкого оператора, где его визуально можно спутать с другим оператором (-)
            6) Явные вызок GC.Collect() плохо пахнет даже с отговорками

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

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

            Вообще, оглядываясь назад, на большинстве моих собеседований пытались узнать чего я не знаю, а не что мне известно. В большинстве ситуаций это просто было своего рода рычагом, чтобы снизить требуюмую изначально ЗП. Не так важно понимать, что человек знает, сколько что он может/умеет делать. Ведь не всегда наличие знаний подразумевает умение ними пользоваться… В этом контексте большинство нынешних собеседований не так показательны, к сожалению…
    • 0
      public static int CastToInt(T value)
      {
      return (int)(dynamic)value;
      }

      Скомпилируется и скастит, скажем, double в int. Для остального (типа стринга) пошлёт с эксешпеном :)
      • +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>. А при первом вызове к тому же будут созданы новые объекты.

    • +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();
      }
      • 0
        Да, это правильный вариант "__refvalue(__makeref(v), int)".
        Других вариантов без лишних аллокаций нет.
        • +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
            Это сравнение того кода который сгенерит JIT компилятор.
            Ему уже известно что вместо дженерика будет int, и кастить int в int он не будет.
            Но, этот компилятор не будет, или в этой версии не будет. Это надежда на оптимизации компилятора. Не спорю что полезно о них знать, но оптимизации могут и не случиться, и приложение начнет засирать память.

            Хотя наблюдение довольно интересное.
  • 0
    уладено. опять неправильно решил последнюю
  • +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 }
    • 0
      Вас не смущает, то что весь цикл while можно убрать и ваш тест не изменится?
      • 0
        Точнее заменить while на if. Спасибо, сразу не сообразил.
        • +1
          Уж лучше оставить while. Здесь проблема в том, что имея такой генератор случайных чисел, можно с какой-то вероятностью, большей чем 0.3 предсказывать значение следующего числа.
  • 0

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

    • 0
      0 будет выпадать с вероятностью 0.5, 1 и 2 по 0.25. Совсем не то, что просили
  • +1
    Вопрос по третьему пункту. Зачем нужен MulticastDelegate, если у обычного делегата уже есть Cobine(+=)?
    • 0
      Цитата:
      Любые типы делегатов — это потомки класса MulticastDelegate, от которого
      они наследуют все поля, свойства и методы.

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

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