Почитав 10 простых задач на c# с подвохом я огорчился т.к. по сути своей там и подвохов-то не было особо (этак можно скатиться до "чему будет равно i++ + ++i")… Посему решил немного повспоминать подвохи, которые не хотел бы видеть никогда в жизни 8-). Уровень подготовки middle наверно.
Многое может оказаться жутким баяном. Конечно, примеры не мои, но мной изучены (и результат ниже) (если авторы (они мне и не известны часто) хотят упоминания их как первооткрывателей — пишите в личку — обновлю пост).
Помните что писал программист — я попытался донести некоторую суть до тех кто прочитает, но язык сух и скучен. Ну и конечно это лишь то что пришло в голову минут за 20.
Ну и конечно это холиварный топик, скорее даже заметка чтобы "не отекли мозги". Тут даже фотки котяток нет.
Готовы? Тогда поехали...
Что будет выведено на экран?
Можно ли создать программу, где используется await для метода, который возвращает не Task?
Скорее практическая задача, которая некоторых ставит в тупик, чем задача с внезапностями.
Можно ли "научить" асинхронности старые (.net2) классы, которые имплементируют паттерн IAsyncResult* без изменения их кода?
*) под паттерном IAsyncResult подразумевается наличие пары методов вида:
, которые осуществляют выполнение некоторой операции асинхронно. Прочитать подробнее в MSDN.
**) подразумевается что вызывающий, конечно, компилируется в версии, где async\await уже поддерживаются.
Что будет на экране при сборке в release?
Что будет на экране при сборке в release и запуске под дебагом?
Чему равно j?
Можно ли в C# "поковырять" память, которую вы не выделяли*?
*) ну или Можно ли поменять размер (но не выделенную память) уже созданного массива?
Отказ от ответственности
Многое может оказаться жутким баяном. Конечно, примеры не мои, но мной изучены (и результат ниже) (если авторы (они мне и не известны часто) хотят упоминания их как первооткрывателей — пишите в личку — обновлю пост).
Помните что писал программист — я попытался донести некоторую суть до тех кто прочитает, но язык сух и скучен. Ну и конечно это лишь то что пришло в голову минут за 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:
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.
Можете попробывать запустить и проверить.
Суть этого явления описана разделе "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 компилятор использует утиную типизацию для поиска кандидатов для вызовов в генерируемом конечном автомате, то задача выльется в простой подбор нужных условий.
Сходу можно соорудить такую реализацию:
А можно вспомнить, что правило допускает использование методов расширений для тех же целей и написать так:
Если вспомнить что при встрече слова 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-ом также разный:
Впрочем, эти сведения могут быть неверными — версии могут меняться, равно как и настройки JIT-а на каждой конкретной системе (исходников-то полноценных нет).
В целом проблема (и её корни) описаны у Сергея Теплякова) в блоге.
Поэтому однозначного ответа на этот вопрос не может быть.
Так например, в реализации 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
вспоминая правила умножения получим результат.
Напомню что старший бит там означает знак. Например для байта 127+1 = 128, 128 = 0x80 и в знаковом представлении это -128.
Или в битах:
-128 = 1000 0000
127 = 0111 1111
-1 = 1111 1111
вспоминая правила умножения получим результат.
Задача 6
Можно ли в C# "поковырять" память, которую вы не выделяли*?
*) ну или Можно ли поменять размер (но не выделенную память) уже созданного массива?
Подробности и ответ
Ну вообще можно.
Аналогично можно проворачивать и другие хитрые фокусы, например со строками — главное знать как они устроены внутренне, а с этим вам легко поможет windbg.
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.