Pull to refresh

Comments 86

Помню, что, когда меня интервьюировали после университета, то тоже спрашивали такие вопросы. На самом деле вопрос был про виртуальные методы и оператор new, которого тут нет. У меня был простой ответный вопрос «А что, вы так пишите?».
для вопросов про сортировку такой ответ ещё хоть как-то канает. в данном случае не канает совсем, ибо во первых чужой код МОЖЕТ быть написан именно так. а во-вторых бывают случаи, когда имеет смысл написать именно так.
а во-вторых бывают случаи, когда имеет смысл написать именно так.


Это какие?
Как ни странно, но goto в C# все еще поддерживается. Хотя не особо нужен.

Вообще-то нужен. Конструкция такого вида недопустима в C# (согласно требованиям C# конец разделов switch должен быть недостижим)

switch(variable)
{
    case 1:
        Console.WriteLine("case 1");
        //need break, goto case, return or throw.
    case 2:
        Console.WriteLine("case 2");
        break;
    default:
        Console.WriteLine("default");
        break;
}

В MSDN предлагают использовать оператор goto в таких случаях. В итоге выходит такой код:

switch(variable)
{
    case 1:
        Console.WriteLine("case 1");
        goto case 2;
    case 2:
        Console.WriteLine("case 2");
        break;
    default:
        Console.WriteLine("default");
        break;
}
Такого рода конструкции не зря недопустимы в языке. Потому что, модифицируя код для case 2 вы на самом деле модифицируете его и для другого случая, т.е. код вводит в заблуждение. Возможно, это когда-то очень нужно (честно говоря, не представляю случая, когда такая необходимость была бы неохбодима), но её следует всеми силами избегать. У Макконнела конец 15-й главы посвящён этому вопросу.

P.S. А второй пример в MSDN вообще ужасен. Вместо того, чтобы выделить цикл в отдельный метод и делать из него return, используют goto в место, которое даже не находится сразу после цикла.
не представляю случая, когда такая необходимость была бы неохбодима

Очень просто: когда один кейс представляет собой расширенный вариант другого. В этом случае можно сначала сделать дополнительную часть, а потом перейти к общей.
Под «необходима» я имел ввиду буквальное «нельзя обойтись без неё». Этот вариант очевиден, более того, он напрашивается. Но опять-таки, то, что очевидно в момент написания будет неочевидным при поддержке.
Обычно такие вещи пишут в коде, который прямо перед глазами (более того, это тот случай, когда я жалею об отсутствии fall-through).
не представляю случая, когда такая необходимость была бы неохбодима

Типичный пример, когда это необходимо — конечный автомат. Наши кейсы в switch структуре будут состояниями, между которыми бы будем перескакивать в соответствии с логикой работы. Другое дело, что без goto можно и в этом случае обойтись — достаточно завернуть switch в while цикл, и устанавливать переменную для следующей итерации. Таким образом, goto case 2: превратится грубо говоря в variable = 2; break; и на следующей итерации мы туда и попадём. Я вот тоже не сторонник goto, этот оператор годится для несложных и «очевидных» случаев, а при разрастании кода обилие goto превратит код в спагетти, поэтому в сложных ситуациях лучше обойтись более продвинутыми паттернами (иногда используют итераторы, либо System.Activities.Statements.StateMachine, и т.д.).
Если у нас примитивный конечный автомат, который точно-точно не прибавит состояний, то возможно. И то, я бы выбрал табличный способ. А для более-менее сложных КА есть паттерн Состояние.
Это для вот таких случаев. Выделять здесь цикл в отдельный метод нельзя — его вызов будет некорректен в любом случае.
            try{
                myEvent1.Reset();
                myEvent2.WaitOne();
               
                for (;;){
                    for (;;){ goto labelExit;
                    }
                labelExit:;
                }
            }
            finally{
                myEvent1.Set();
            }
Для вот таких вот случаев есть break; и goto тут не обязательно использовать. А вот если нужно выскочить сразу за пределы всех циклов, тогда да, можно и воспользоваться.
А почему нельзя так?
switch(variable)
{
case 1:
case 2:
Console.WrileLine("case 1");
Console.WrileLine("case 2");
break;
default:
Console.WrileLine("default");
break;
}
В целом, так можно, но не в данном случае: при case 2 будет выводить case1 + case2. Очевидно, что это не то, что хотелось бы.
Объясните джависту:

2,3. Почему так? У нас наоборот.
7. А почему это вообще работает? Ведь в момент вызова делегата i уже не существует, мы ведь вышли из ее области видимости.
7. Тут важно разрушается ли переменная i в конце итерации, или переиспользуется. В данном случае переиспользуется. Чтобы исправить ситуацию достаточно либо завести новую переменную внутри цикла, либо использовать foreach (int i in Enumerable.Range(0, 10)) начиная с C# 4.
Я о другом. Почему [10, ..., 10], а не [0, ..., 9] — это понятно. Непонятно почему оно в текущем виде компилируется и работает, а не говорит что-то в духе «в момент вызова делегата переменной i уже не будет существовать, так что иди нахрен».

Другими словами, почему последнее i=10 сохраняется, а не разрушается.
Ну потому что переменная i в таком случае будет находиться в куче, в инстансе специалного класса замыкания, и будет уничтожена только после GC.

Но любой нормальный статический анализатор подскажет, что здесь нечисто.
Вот я и негодую что язык такое позволят. Ибо очевидно что что-то не так.
Наверное на то есть причина. Потому как у соседнего тут Go точно такая же «фича» — замыкание захватывает ссылку, GC доволен и получается тот же самый результат. Тоже фигурирует в такого рода списках, что нужно просто знать, чтобы потом не было неприятно.
А какие ещё опции? Как по-другому такой код написать?
Func<int> F(int i) { return () => i; }
Можете пояснить, что вы имеете ввиду? Неясный синтаксис.
Я так понимаю, вы имеете ввиду что-то вроде:

Func F = () => i;

Но это, по сути, то же самое, что и с делегатом выше.
Есть мысль, что захватывать можно только константы.
Есть мысль, что захватывать можно только константы.

А смысл тогда их захватывать?
Эмн. Ну, например, для того, что написано в комментарии, на который я отвечал. Там, относительно замыкания, i — это константа.
Я не знаю, что такое «константа относительно замыкания», я отвечал именно относительно констант.
Идеологически оно может и верно, но практическая польза от замыканий при этом падает.
Ок, признаю, я действительно очень неудачно выбрал термин. Имел ввиду не static final из java или const из c++, а просто что-то неизменяемое в данной области видимости. Т.е. очевидно, что в вашем примере i — это переменная, и может принимать разные значения, но внутри метода F меняться значение не может. Так что мы можем создать замыкание и утащить туда конкретное значение.

Возвращаясь к исходному примеру. Я бы понял, если бы на каждой итерации это были бы разные неизменяемые i. В таком случае, правильным был бы вывод [0, ..., 9]. Но вот в текущей реализации, я считаю, что создавать замыкание i (хз как правильно, «над i», «с i») нельзя. И следить за этим должен компилятор.
Слишком много магии, определение изменяемости/неизмениемости переменных. Предложение об изменении типа переменной в зависимости от скоупа if (a is B) { /* переменная a типа B */ } по этой же самой причине отклонили. Нельзя так просто взять и узнать, модифицируется ли переменная в скоупе, или нет. Вместо этого будет использоваться синтаксис if (a is B b), создающий новую переменную, что и рекомендуется делать, если вы запутались с замыканиями.

Кстати, язык С++ даёт вам контроль над этим моментом, позволяя захват как по ссылке (как в нормальных языках), так и по значению (копируя данные).
1. Какой магии? Если компилятор может доказать что присваивание ровно одно — значит неизменяемая. Во всех остальных случаях считаем изменяемой. Например понятие «effectively final» из Java8: «A variable or parameter whose value is never changed after it is initialized is effectively final.». Еще ни разу не сталкивался с багами в этом месте. Естественно, я не говорю про C++, но в C#, на сколько я знаю, прямой работы с памятью нет, так что отследить изменения всегда должно быть возможно.

2. Kotlin, Smart Casts.

оффтоп
Нельзя так просто взять и узнать

Дурацкие мемы, автоматически в голове этот текст рисуется на соответствующем фоне.
Если компилятор может доказать что присваивание ровно одно — значит неизменяемая. Во всех остальных случаях считаем изменяемой.
Если бы вы открыли статью, на которую я дал ссылку, вы бы увидели там объяснение, почему в C# это не так.

Вкрадце, локальная переменная может находиться на куче, и там её значение может быть изменено по огромному количеству причин. (Обратите внимание на термины «переменная» и «значение» здесь — не путать с значимыми типами, они здесь роли не играют.)
Вы мне не ту часть скинули. В этой написано лишь (по данной теме) «программист и сам в состоянии уследить» (а с таким подходом недолго и до javascript докатиться). Проблемы изменяемости описаны в первой части. Ну так там и речь не про локальные переменные. По сути, про локалы остается лишь перегрузка методов из конца первой части. Там да, признаю, действительно проблема.

Вообще, вижу что в C# с этим действительно сложнее чем в Java. А в котлиновских smart casts так вообще на ряд вещей забили.
внутри метода F меняться значение не может

Вот только возвращенное замыкание существует и после того, как метод F перестал существовать. И если с int все просто, он был скопирован на входе в метод и с тех пор неизменен, то с reference type все намного сложнее.

В реальности, правильная формулировка звучит как «замыкаться надо на immutable», как оно чаще всего в функциональных языках и делается, но в C# этого достичь не так-то просто.
А в референсах скопировали саму ссылку и окей)).

На самом деле, я прекрасно понимаю что при изменением внутреннего состояния объекта по скопированной ссылки совершенно спокойно могут быть (и будут) проблемы, описанные в примере из статьи. Так что в самом правильном варианте — только immutable. Просто если введением local final variable можно убрать если не всю проблему, но хотя бы ее часть, то почему-бы этого не сделать?

А вообще, для данной задачи понятие владения из Rust подходит даже больше чем immutable.
Вполне все логично и обяснимо.
Захват происходит по ссылке. Очевидно что такая переменная выделятся уже не из стека (что характерно для структур), а из кучи и хранится там пока есть хотябы одна ссылка на эту переменную (в данном случае ссылка храниться в делегатах).
Вообще нужно аккуратнее со всякими ref и замыканиями работать
В примерах 2 и 3 в C# так хотя бы потому, что эти методы не виртуальные по умолчанию как в Java. К слову, если попытаться сделать виртуальными, будет ошибка вроде «main.cs(22,30): error CS0115: `Program.B.abc(Program.P)' is marked as an override but no suitable method found to override». C++ себя ведёт так же.
Вопрос в том, почему Java так странно работает, если сделать аннотацию @ Override, то будет ошибка компиляции как в C# (error: method does not override or implement a method from a supertype), иными слова метод наследника не переопределялся, но при этом Java лезет вверх по наследованию для подходящего метода. Чтобы результат был как в C# (abc из B) надо метод базового класса сделать private.
Самое весёлое, что такой код из задачи никто и никогда не стал бы писать.
Не соглашусь, в java как раз все логично (по крайней мере, в данном случае). Метод определяется не только по имени, но и по типам параметров. Соответственно abc(int) и abc(double) — это разные методы. Потому и не переопределяется. Вот если в наследнике тоже сделать abc(int), то будет честное переопределение и напечатается B.

надо метод базового класса сделать private

Ну тогда он на результат вообще никак влиять не будет. Независимо от класса B. Так что явно плохое исправление.

Самое весёлое, что такой код из задачи никто и никогда не стал бы писать.

Безусловно. Просто хотелось понять непонятный результат.
Вообще при поиске метода, должна учитываться полная сигнатура, и в данном случае у класса B есть две публичных перегрузки, abc(int) и abc(double). Почему компилятор выбирает вторую для вызова при наличии подходящей сигнатуры abc(int), очень хороший вопрос, и стоит пожалуй с ним подробнее разобраться. Виртуальность здесь совершенно не причем.
А вот и пояснения странному поведению вопроса 2: http://csharpindepth.com/Articles/General/Overloading.aspx
А вот то, что даже при переопределении метода с параметром int вызывается всё равно с double — вообще не круто =\
Благодарю, очень интересный нюанс.
по поводу номера 2
На С++ тоже так. Честно говоря, я даже и не знал о таком, ибо никогда так не напишу… но получается что при выборе метода компилятор считает что double «наследуется» от int ???
Не наследуется, в данном случае используется неявное приведение типа, т.к. операция double a = (int)5 валидна, она же используется при вызове метода.
Вот еще боян :)

static void Main(string[] args)
        {
            double x = secretFunction();
            if (x != x)
            {
                    Console.WriteLine("Вот так вот :D");
            }
            Console.ReadLine();
        }


Выводит «Вот так вот :D»

Привести пример secretFunction.

Ответ
public static double secretFunction()
{
    return 0.0/0.0;
}

Или
Ответ
Более явно:
public static double secretFunction()
{
    return double.NaN;
}

ну это вопрос в вопросе для подсмотревших, понять, как оно такое вышло) а ваш вариант интригу убивает
Извините, не думал что это специально. Я бы сразу выдал подробный ответ.
Ну уж если быть точным то в 1

1) «Обе переменные не инициализированы», это ложное утверждение, поля класса инициализируются в значения по умолчанию (они же нули) при создании экземпляра, или в данном случае при создании экземпляра типа.

2) «если быть более точным, то это immutable тип, что означает reference тип с семантикой value типа», в данном контексте тоже бред, да и вообще бред, что значит с семантикой value типа? Значение локальных переменных области видимости, хранится на стеке? нет! В методы передается копия? Нет!
Хочу еще вопросов, разных, красных синих, и малиновых. Довольно занимательные задачи.
Было бы хорошо если бы было объяснение для каждого…
Чтобы консоль не закрывалась и можно было посмотреть результат.
Слушай, ну я знаю про Ctrl-F5, но спрашивали же зачем ридлайн. Я объяснил зачем он по моему мнению.
http://slonopotamus.org/console.png

Ничего не закрывается. Что я делаю не так?
> float и double не являются встроенными типами данных (integral types)

такой перевод (или его применение) коробит
float и double настолько же встроенные типы как и int
лучше integral types так и называть «интегральные типы» или целые (включая char & bool)
https://msdn.microsoft.com/ru-ru/library/ya5y69ds.aspx

P.S. интегральные + числа с плавающей точкой = арифметические
Поразвлекаться неплохо, но если вы реально принимаете решение брать или не брать человека на основе этих вопросов, то это ппц. Особенно эта каша из классов A, B, C с наследованием.
Есть основы, которые надо знать (типа отличния абстстрактного класса и интерфейса), а есть попытки заставить кандидата компиллировать говнокод в голове.
Я в таких случаях в ответ спрашиваю правда ли такой код встречается в репозитории. И если да, то для меня это сигнал не идти сюда.
Дело не в коде, а в понимании того, как работает тот инструмент который вы используете, без понимания даже следуя лучшим практикам можно сделать такое интересное решение.
Скорее процент правильных ответов это очень косвенный признак того, сколько лет автор провёл, читая код самых разных проектов. Что, конечно, приведёт к печальному результату собеседования на Junior программиста.

Хотя… с практической точки зрения это можно использовать чтобы деморализовать претендента и заставить его подписать трудовой договор на меньшую з/п.
Так и не понял, при чем тут код разных проектов? Я меня не так много проектов, которые я реально читал, почти все задачи для меня понятны. А не понятные в свою очередь интересны, вот вы например знали почему в 2 и 3 получается такой результат? Я не знал и не жалею, что потратил время, на то что бы разобраться.
Так и не понял, при чем тут код разных проектов?
Чтобы слёту ответить на все вопросы, на мой взгляд, нужно иметь опыт, уже набив шишки на подобных чьих-то решениях в чужих проектах. Так как вопросы на мой взгляд не совсем тривиальные и даже сомнительные (в плане целесообразности использования), может понадобиться много времени, чтобы повстречать их ВСЕ в тех или иных реальных проектах. Почему в проектах и почему реальных? Потому что лично я не думаю, что можно заполучить соответствующие знания, лишь просто прочитав несколько книг.
Знания достаточные для правильного ответа на все задачи можно получить из одной книги C# Spec
Не соглашусь, вот например задачка 2 ну и 3 за одно, очень интересной оказалась и реально может устроить в реальной жизни сюрприз.
А теперь более каверзный вопрос на тему вопроса номер 7.
delegate void SomeMethod();

static void Main(string[] args)
{
            List<SomeMethod> delList = new List<SomeMethod>();
            foreach (int i in System.Linq.Enumerable.Range(1, 10))
            {
                delList.Add(delegate { Console.WriteLine(i); });
            }

            foreach (var del in delList)
            {
                del();
            }
}


Ответ
Правильный ответ: какую версию C# вы используете? При C# >= 5: будут выведены числа от 1 до 10, для более старых — десять десяток.
Да, это breaking changes, можно почитать stackoverflow.com/questions/12112881/has-foreachs-use-of-variables-been-changed-in-c-sharp-5
А теперь поясните зачем нормальному человеку в 2016 пользоваться delegate, когда есть
Func<T>
и прочие.
Если вне контекста задачи — то для событий, например. А в контексте — ну делегат и делегат, от замены на Action/Func концептуально ничего не поменяется.
Если честно, никогда не сталкивался с этой проблемой в реальной жизни. Зато каждый, кто прочитал про closure, норовит сунуть этот пример в интервью (обязательно с циклом for).
Я не спорю что знать полезно. Просто покажите мне человека, который столкнулся с этим в реальном проекте.
Я-таки сталкивался, но с вами абсолютно согласен. Большинство задач (да и практик) «хитрого кода» — от лукавого.
Я просто написал пример по образу и подобию примера в статье, так-то там лямбду надо использовать и Action.
А вот такой код выдаст 1 2 3 на любой версии языка
 static void Main(string[] args)
        {
            var delList = Enumerable.Range(1, 3)
                .Select(i => (Action) delegate { Console.WriteLine(i); });

            foreach (var del in delList )
            {
                del();
            }
        }


Ибо Linq ленив и замыкание здесь будет стряпаться прямо перед выполнением
Так, а я вот даже не поленился, сменил версию языка на C# 4.0, и для верности даже версию фреймворка на 3.5, но foreach в лбом варианте упорно выдает 1 2 3
Что-то тут нечисто. Сижу в 2015 студии
2015 студия использует компилятор Roslyn даже для предыдущих версий языков и фреймворков.
Тут, вроде, нет замыкания. Просто i передаётся как параметр в функцию, где благополучно копируется по значению и выводится.
Здесь нет замыкания, ну прям совсем нет.
Ну я даже не знаю, можете декомпилировать и проверить. В .NET нет инструментов, чтобы оптимизировать этот C# код с замыканием таким образом, чтобы он выполнялся без замыкания.
Где тут замыкание?

var delList = Enumerable.Range(1, 3).Select(i => (Action) delegate { Console.WriteLine(i); });
Создаётся через ключевое слово delegate.
Да действительно я ступил, тут есть замыкание, но когда происходит захват контекста в данном случае i, мы имеем захват контекста вызова i =>… что приводит к тому, что замыкание каждый раз захватывает не внешнюю переменную, а значение i на момент вызова, т.е. по факту происходит захват значения 1, 2, 3, 4, а не захват внешней переменной.

var smth = 0;
var delList = Enumerable.Range(1, 3).Select(i => { smth = i; return (Action) delegate { Console.WriteLine(smth); }; });
Ну еще .ToList() надо для порядка иначе отображение совпадет с передачей значения.
Да, я конечно же говорю про вариант с .ToList(), он тоже выдает 1 2 3. И в 2013 студии, кстати тоже 1 2 3
var smth = 0;
var delList = Enumerable.Range(1, 3).Select(i => { smth = i; return (Action) delegate { Console.WriteLine(smth); }; }).ToList();

Выдает 3 3 3 т.к. происходит захват переменной smth. А ее значение к моменту вызова делегата с замыканием уже 3 и не меняется, что собственно происходит в примере приведенном в статье.
Sign up to leave a comment.

Articles