Pull to refresh

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

Reading time 7 min
Views 38K
Почитав 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.
Tags:
Hubs:
+37
Comments 15
Comments Comments 15

Articles