Monday, June 25, 2007

Interesting code puzzles

Периодически встречаю разные интересные приколы интересного неоднозначного кода. Интересные они не только тем, что однозначно не скажешь результат кода, а ещё и тем, что можно случайно и в реальной жизни нарваться на грабли от таких приколов.

Вот и решил собрать небольшую подборочку таких приколов. Если ещё найду, буду дополнять.

Начнём с простеньких. Возможно кому-то они покажутся слишком, но меня удивили.

Какой метод будет вызван?

(C#, на Java вроде то же самое)

using System;

namespace ConsoleApplication2
{
class Program
{
private static void A(Object o)
{
Console.WriteLine("Object");
}
private static void A(String s)
{
Console.WriteLine("String");
}
static void Main(string[] args)
{
A(null);
}
}
}


Ответ: "String". Потому, что String наследуется от Object, а null можно привести к String. т.е. полностью однозначный вызов метода с параметром наиболее "высокого" в иерархии классов параметра.


Пока не нашёл точной спецификации в MSDN описания этого поведения. Подредактирую этот пост как найду.



Инкриментинг


(C#, на Java же самое)

using System;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
int tricky = 0;
for (int i = 0; i < 3; i++)
tricky += tricky++;
Console.WriteLine(tricky);
}
}
}


Ответ: 0. Потому, что каждый раз к 0 будет прибавляться 0, с постинкримент будет происходить до изменения переменной.


Кстати, интересно то, что если в C/C++ выражение i=i++ является неопределённым (т.е. выдаёт на разных компайлерах разные результаты - см п 3.8), то в C#/Java всё довольно чётко определенно.



Массив


(C#, на Java же самое)

using System;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
int[] arr = new int[2] { 0, 1} ;
int i = 0;
try
{
arr[i] = i = 2;
Console.WriteLine("{" + arr[0] + "," + arr[1] + "} i=" + i);
} catch (IndexOutOfRangeException)
{
Console.WriteLine("Out of bounds!");
}
}
}
}

Ответ:



{2,1} 2

При компиляции операция "=" обрабатывается слева на право - т.е. сначала занесёться в стек текущее значение i, потом произойдёт изменение i и уже после этого будет непосредственное изменение массива.


Равенства


Думаю всем известно, что в Java и в C# есть разные способы сравнивать. Вот интересный и простой тест (хотя в Java есть только два способа из-за невозможности перегрузки операторов). Первая часть скорее проверка базовых знаний, а вторая уже немного интересней.

using System;
namespace ConsoleApplication
{
class Program
{
public static void Check(object a, object b)
{
Console.Write(a == b ? "same" : "not same");
Console.Write(a.Equals(b) ? " and equals" : " not equals");
Console.Write(object.ReferenceEquals(a, b) ? " and ref equals" : " not ref equals");
Console.WriteLine();
}

static void Main(string[] args)
{
int inta = 1000;
int intb = 1000;
Check(inta, intb);

String stringA = "test";
String stringB = "test";
Check(stringA, stringB);

}
}
}

Ответ:


not same and equals not ref equals
same and equals and ref equals
Для int'ов - ссылки разные из-за того, что произошел boxing, а для строк ссылки одинаковые из-за того, что компилятор одинаковые строки собирает, и при инициализации ссылки на строку передаёт один и тот же объект.


В принципе если посмотреть на сборку, которая получается в результате, то объяснение ответа второго вывода довольно простое.


Инициализация


Этот пазл имхо уже довольно сложный. И снова таки как и в C# так и в Java поведение его одинаково.


Вот собственно код:

using System;
using System.Threading;

namespace ConsoleApplication
{
class Program
{
public class TestingClass
{
public static bool inited = false;
static TestingClass()
{
Thread thr = new Thread(new ThreadStart(delegate { inited = true; }));
thr.Start();
thr.Join();
}
}


static void Main(string[] args)
{
Console.WriteLine("Lets check if TestingClass was inited:");
Console.WriteLine(TestingClass.inited);
Console.WriteLine("Done.");
}
}
}


Ответ:


Программа войдёт в dead-lock: во время инициализации класса будет вызван статический конструктор класса. Внутри которого создаёться поток, в котором выполняется код клоужера, который обращаеться к статическому ствойству класса. Но так, как класс ещё не прошёл инициализацию, а доступ происходит из другого потока, то этот поток будет ждать пока завершиться инициализация, т.е. завершится выполнятся статический конструктор. А конструктор-то ждёт завершение потока. Т.е. получаем классический dead-lock.



PS. Ах да, некоторые идеи а так же Java варианты некоторых пазлов можно посмотреть тут

3 comments:

Andrey said...

Я так понимаю вы с Java всё же не в очень в тесных отношениях)

В паззле про равенство вывод для Java будет таким:
same and equals
same and equals

Они там, видать, ещё на этапе разработки зрозумили что autoboxing довольно-таки тормознутая штука - поэтому для Integer целые значения -128..127 закешированы.

И ещё, 3-е сравнение в функции Check. Что-то не могу найти(припомнить)аналога для Java - что там вообще проверяется?

Olostan said...

Andrey, Вы всё верно заметили.

Действительно забыл дописать оговорку про кэширование примитивов (на самом деле, если не ошибаюсь, это кэширование не является необходимым требованием для JRE, прошу поправить если не так). Исправлю "5" на "1000" :)

Про ReferenceEquals: так, как в Java нет перегрузки операторов (что есть фактически "синтаксический сахар"), то == всегда является аналогом ReferenceEquals. Так что, действительно, в Java варианте есть только два сравнения.

Anonymous said...

Ну и последний пример - статический конструктор... Нету такого в джаве