6 простых вопросов по C# с подвохом

    Почитав 10 простых задач на c# с подвохом я огорчился т.к. по сути своей там и подвохов-то не было особо (этак можно скатиться до "чему будет равно i++ + ++i")… Посему решил немного повспоминать подвохи, которые не хотел бы видеть никогда в жизни 8-). Уровень подготовки middle наверно.


    Отказ от ответственности


    Многое может оказаться жутким баяном. Конечно, примеры не мои, но мной изучены (и результат ниже) (если авторы (они мне и не известны часто) хотят упоминания их как первооткрывателей — пишите в личку — обновлю пост).
    Помните что писал программист — я попытался донести некоторую суть до тех кто прочитает, но язык сух и скучен. Ну и конечно это лишь то что пришло в голову минут за 20.

    Ну и конечно это холиварный топик, скорее даже заметка чтобы "не отекли мозги". Тут даже фотки котяток нет.

    Готовы? Тогда поехали...


    Задача 1


    Что будет выведено на экран?
    using System;
    using System.Xml;
    
    public class Program
    {
        public static void Main()
        {
            Bar(XmlWriter => XmlWriter.Flush());
            Bar(XmlReader => XmlReader.Flush());
        }
    
        private static void Bar(Action<XmlWriter> x)
        {
            Console.WriteLine("W");
        }
    
        private static void Bar(Action<XmlReader> x)
        {
            Console.WriteLine("R");
        }
    }
    

    Подробности и ответ
    Выведется W W.
    Можете попробывать запустить и проверить.

    Суть этого явления описана разделе "7.6.4.1 Identical simple names and type names" спецификации. Для начала я приведу этот раздел целиком:
    In a member access of the form E.I, if E is a single identifier, and if the meaning of E as a simple-name (§7.6.2) is a constant, field, property, local variable, or parameter with the same type as the meaning of E as a type-name (§3.8), then both possible meanings of E are permitted. The two possible meanings of E.I are never ambiguous, since I must necessarily be a member of the type E in both cases. In other words, the rule simply permits access to the static members and nested types of E where a compile-time error would otherwise have occurred. For example:
    struct Color
    {
    	public static readonly Color White = new Color(...);
    	public static readonly Color Black = new Color(...);
    	public Color Complement() {...}
    }
    class A
    {
    	public Color Color;					// Field Color of type Color
    	void F() {
    		Color = Color.Black; 			// References Color.Black static member
    		Color = Color.Complement();	// Invokes Complement() on Color field
    	}
    	static void G() {
    		Color c = Color.White;			// References Color.White static member
    	}
    }
    

    Within the A class, those occurrences of the Color identifier that reference the Color type are underlined, and those that reference the Color field are not underlined.

    Кратко суть происходящего можно описать примерно так: компилятор в случае когда имя переменной совпадает с именем типа будет пытаться найти статические члены или субклассы с именем (в определении типа), заданным после точки, если переменная не содержит членов с таким именем. При ненахождении выдаст ошибку компиляции, конечно.
    Именно это правило и даёт этот эффект: т.к. у XmlReader-а нет метода Flush() (в отличии от XmlWriter), то компилятор выводит тип делегата в обоих случаях как Action<XmlWriter> и вызывает соотвествующий подходящий метод, который и выводит W.


    Задача 2


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

    Подробности и ответ
    Да, можно. И путей для реализации этого несколько.
    Если вспомнить что при встрече слова await компилятор использует утиную типизацию для поиска кандидатов для вызовов в генерируемом конечном автомате, то задача выльется в простой подбор нужных условий.

    Сходу можно соорудить такую реализацию:
    using System.Runtime.CompilerServices;
    
    class Program
    {
        private static void Main()
        {
            MainAsync();
        }
    
        private async static void MainAsync()
        {
            await Foo();
        }
    
        static Target Foo()
        {
            return new Target();
        }
    }
    
    class Target
    {
        public TaskAwaiter GetAwaiter()
        {
            return new TaskAwaiter();
        }
    }
    


    А можно вспомнить, что правило допускает использование методов расширений для тех же целей и написать так:
    using System.Runtime.CompilerServices;
    
    class Program
    {
        private static void Main()
        {
            MainAsync();
        }
    
        private async static void MainAsync()
        {
            await Foo();
        }
    
        static Target Foo()
        {
            return new Target();
        }
    }
    
    class Target
    {
        
    }
    
    static class TargetEx
    {
        public static TaskAwaiter GetAwaiter(this Target t)
        {
            return new TaskAwaiter();
        }
    }
    



    Задача 3


    Скорее практическая задача, которая некоторых ставит в тупик, чем задача с внезапностями.
    Можно ли "научить" асинхронности старые (.net2) классы, которые имплементируют паттерн IAsyncResult* без изменения их кода?
    *) под паттерном IAsyncResult подразумевается наличие пары методов вида:
    IAsyncResult BeginXXX(AsyncCallback callback);
    void EndXXX(IAsyncResult);
    

    , которые осуществляют выполнение некоторой операции асинхронно. Прочитать подробнее в MSDN.
    **) подразумевается что вызывающий, конечно, компилируется в версии, где async\await уже поддерживаются.
    Подробности и ответ
    Да, можно написать расширение класса (в новой версии) и использовать TaskCompletionSource для исполнения (или Task.Factory.FromAsyncPattern, но это не совсем одно и тоже).

    К примеру:
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    
    class Program
    {
        private static void Main()
        {
            MainAsync();
            Console.ReadLine();
        }
    
        private async static void MainAsync()
        {
            var ogc = new OldGoodClass();
            await ogc.OperationAsync().ConfigureAwait(false);
        }
    }
    
    
    static class OldGoodClassEx
    {
        public static Task OperationAsync(this OldGoodClass ogc)
        {
            var tsc = new TaskCompletionSource<object>(ogc);
            AsyncCallback onDone = (ar) =>
            {
                ogc.EndOperation(ar);
                tsc.SetResult(null);
            };
            ogc.BeginOperation(onDone);
            return tsc.Task;
        }
    }
    
    
    
    class OldGoodClass
    {
        class AsyncResult : IAsyncResult
        {
            #region Implementation of IAsyncResult
    
            public bool IsCompleted { get; set; }
            public WaitHandle AsyncWaitHandle { get; set; }
            public object AsyncState { get; set; }
            public bool CompletedSynchronously
            {
                get { return false; }
            }
    
            #endregion
        }
    
        public IAsyncResult BeginOperation(AsyncCallback onDone)
        {
            var rv = new AsyncResult();
            ThreadPool.QueueUserWorkItem(s =>
            {
                Thread.Sleep(2000);
                var ar = (AsyncResult) s;
                ar.IsCompleted = true;
                if (onDone != null) onDone(ar);
            }, rv);
            return rv;
        }
    
        public void EndOperation(IAsyncResult r)
        {
            while (!r.IsCompleted) { }
        }
    }
    



    Задача 4


    Что будет на экране при сборке в release?
    Что будет на экране при сборке в release и запуске под дебагом?
     using System;
    
    
    internal class Program
    {
        private class MyClass
        {
            public MyClass()
            {
                Console.WriteLine("ctor");
    
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
    
            ~MyClass()
            {
                Console.WriteLine("dtor");
            }
        }
    
    
        private static void Main(string[] args)
        {
            var myClass = new MyClass();
            if (myClass != null)
            {
                Console.WriteLine("not null");
            }
            else
            {
                Console.WriteLine("null");
            }
    
        }
    }
    


    Подробности и ответ
    Суть заключается в том что необязательно в JIT-е может быть реализована возможность поиска области видимости переменной внутри метода и, как следствие, её уничтожение посредством GC.
    Поэтому однозначного ответа на этот вопрос не может быть.
    Так например, в реализации JIT-а Misrosoft под Windows (клиентский JIT) в версии .net4 эта возможность реализована так:
    — в release сборке, запущенной без дебага, используются эти самые «регионы»,
    — в release сборке, запущенной под дебагом, область видимости продлевается до конца метода.
    Например на .net4.5 без дебага (при сборке в release режиме) будет выдано [ctor, dtor, not null], в отличие от под дебагом той же сборки: [ctor, not null] (Не видите подвох? Вдумайтесь в порядок того что вывелось).
    Код, созданный JIT-ом также разный:
    Подробности x86 ассемблера
        >>> 002800D8 55               push        ebp
        002800D9 8BEC             mov         ebp,esp
        002800DB 83EC0C           sub         esp,0Ch
        002800DE 33C0             xor         eax,eax
        002800E0 8945F4           mov         dword ptr [ebp-0Ch],eax
        002800E3 894DFC           mov         dword ptr [ebp-4],ecx
        002800E6 833D6031150000   cmp         dword ptr ds:[00153160h],0
        002800ED 7405             je          002800F4
        002800EF E83A796362       call        628B7A2E (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
        002800F4 33D2             xor         edx,edx
        002800F6 8955F8           mov         dword ptr [ebp-8],edx
        002800F9 B918381500       mov         ecx,153818h (MT: ConsoleApplication11.Program+MyClass)
        002800FE E8C9833A62       call        626284CC (JitHelp: CORINFO_HELP_NEWFAST)
        00280103 8945F4           mov         dword ptr [ebp-0Ch],eax
        00280106 8B4DF4           mov         ecx,dword ptr [ebp-0Ch]
        00280109 FF1538381500     call        dword ptr ds:[00153838h] (ConsoleApplication11.Program+MyClass..ctor(), mdToken: 06000004)
        0028010F 8B45F4           mov         eax,dword ptr [ebp-0Ch]
        00280112 8945F8           mov         dword ptr [ebp-8],eax
        00280115 837DF800         cmp         dword ptr [ebp-8],0
        00280119 7410             je          0028012B
        0028011B 8B0D38213803     mov         ecx,dword ptr ds:[03382138h] ("not null")
        00280121 E8FACD6561       call        618DCF20 (System.Console.WriteLine(System.String), mdToken: 06000993)
        00280126 90               nop
        00280127 8BE5             mov         esp,ebp
        00280127 8BE5             интересно а кто-то вообще обращает внимание что тут написано?)
        00280129 5D               pop         ebp
        0028012A C3               ret
        0028012B 8B0D3C213803     mov         ecx,dword ptr ds:[0338213Ch] ("null")
        00280131 E8EACD6561       call        618DCF20 (System.Console.WriteLine(System.String), mdToken: 06000993)
        00280136 90               nop
        00280137 8BE5             mov         esp,ebp
        00280139 5D               pop         ebp
        0028013A C3               ret
    

    против
    002F0098 55               push        ebp
    002F0099 8BEC             mov         ebp,esp
    002F009B B924381900       mov         ecx,193824h (MT: ConsoleApplication11.Program+MyClass)
    002F00A0 E827843362       call        626284CC (JitHelp: CORINFO_HELP_NEWFAST)
    002F00A5 8BC8             mov         ecx,eax
    002F00A7 FF1544381900     call        dword ptr ds:[00193844h] (ConsoleApplication11.Program+MyClass..ctor(), mdToken: 06000004)
    002F00AD E892CE5E61       call        618DCF44 (System.Console.get_Out(), mdToken: 06000946)
    002F00B2 8BC8             mov         ecx,eax
    002F00B4 8B1538217803     mov         edx,dword ptr ds:[03782138h] ("not null")
    002F00BA 8B01             mov         eax,dword ptr [ecx]
    002F00BC 8B403C           mov         eax,dword ptr [eax+3Ch]
    002F00BF FF5010           call        dword ptr [eax+10h]
    002F00C2 5D               pop         ebp
    002F00C3 C3               ret
    


    Впрочем, эти сведения могут быть неверными — версии могут меняться, равно как и настройки JIT-а на каждой конкретной системе (исходников-то полноценных нет).
    В целом проблема (и её корни) описаны у Сергея Теплякова) в блоге.


    Задача 5


    Чему равно j?
    Int32 i = Int32.MinValue;
    Int32 j = -i;
    

    Подробности и ответ
    Компилирование в checked контексте выдаст эксепшен. В unckecked получим значение Int32.MinValue. Просто потому что так работают знаковые типы.
    Напомню что старший бит там означает знак. Например для байта 127+1 = 128, 128 = 0x80 и в знаковом представлении это -128.
    Или в битах:
    -128 = 1000 0000
    127 = 0111 1111
    -1 = 1111 1111
    вспоминая правила умножения получим результат.


    Задача 6


    Можно ли в C# "поковырять" память, которую вы не выделяли*?
    *) ну или Можно ли поменять размер (но не выделенную память) уже созданного массива?
    Подробности и ответ
    Ну вообще можно.
     using System.Runtime.InteropServices;
    
    
    class ArrayLength
    {
        public int Length;
    }
    
    
     [StructLayout(LayoutKind.Explicit)]
    class MyArray
     {
         [FieldOffset(0)]
         public ArrayLength ArrayLength;
    
         [FieldOffset(0)]
         public byte[] Array = new byte[4];
     }
    
    internal class Program
    {
        private static void Main(string[] args)
        {
            var arr = new MyArray();
            arr.ArrayLength.Length = 1024;
        }
    }
    

    Аналогично можно проворачивать и другие хитрые фокусы, например со строками — главное знать как они устроены внутренне, а с этим вам легко поможет windbg.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 15
    • 0
      «Можно ли в C# „поковырять“ память, которую вы не выделяли*?»
      А через unsafe-режим разве низя?
      • +5
        Ну unsafe режим это логично и очевидно. Потому он и называется unsafe… Скучно ж.
        • +4
          LayoutKind.Explicit — это ровно тот же unsafe, и то, что компилятор при его использовании не требует соответствующего ключевого слова — это, скорее, недоработка авторов языка. Аналогично Marshal и т.д.
          • +3
            Я отвечал про режим сборки. Так-то если копать то весь .net в конечном итоге опирается на unsafe методы… важнее, конечно, знания вида «будет ли это работать не в fulltrust»…
            А так-то я, конечно, согласен с комментарием.
      • +4
        Чё-то почти все задачи из серии «как почесать левой пяткой правое ухо».
        Ну разве что четвёртая еще туда-сюда, но тоже жутчайшая синтетика: 99% С#-пистов вручную GC не зовут никогда.
        • +1
          Всё что «просто» описано всюду (я даже одну из ссылок привёл для примера). Разве это интересно?)
          GC вызван для демонстрации (ну не ждать же его в демо?).
          • +1
            2 интересна для понимания того как оно работает
            5 просто нужно знать, с этим сталкивался не раз.
            6 полезная штука, применял на практике (для хранения цвета, с «бесплатным» доступом как к байтовым компонентам ARGB, так и в виде uint — производительность была очень важна, и этот подход хорошо помог)
          • –1
            Весь маршал один сплошной ансейв!
            • +2
              > Можно ли создать программу, где используется await для метода, который не помечен как async?
              Всё гораздо проще, даже не нужно выдумывать новые классы:

              private async static void MainAsync()
              {
                  await Foo();
              }

              static Task Foo()
              {
                  return Task.Delay(1000);
              }

              Дело в том, что на возможность await влияет не факт того, помечен или нет целевой метод как async, а возвращаемый тип. Поэтому я бы назвал эту задачу: «Можно ли await метод, который возвращает не Task?».
              • +1
                Да, спасибо. Ошибка в формулировке исправлена 8-)
                • 0
                  Вообще, спасибо, что подняли тему асинхронности. Грустно в последнее время видеть различные туториалы, которые используют I/O (в особенности, сеть) и не используют асинхронность. Учитывая, насколько удобным и прозрачным получился паттерн async/await, в C# подобные вещи, реализованные синхронно, имхо, должны считаться моветоном :)
              • +7
                00280127 8BE5             интересно а кто-то вообще обращает внимание что тут написано?)
                

                Люблю пасхальные яйца
                • –1
                  понял. комент удалил.
                • 0
                  Спасибо за TaskCompletionSource, честно не знал :)
                  • 0
                    Не знал ответа ни на один вопрос, я теперь не могу считать себя middle?)))

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