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


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


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


К сожалению, не доступен сервер mySQL