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 для метода, который не помечен как async?
Конечно речь не идёт о «не своих» классах.

Подробности и ответ

Да, можно. И путей для реализации этого несколько.
Если вспомнить что при встрече слова 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.

ссылка на оригинал статьи http://habrahabr.ru/post/209008/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *