Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Tuesday, June 29, 2010

C#: Invoke method from null-valued variable?

Как-то не задумывался, что конструкции по типу:

A a = null;
a.SomeMethod();


могут не вызывать NullReferenceException, а отрабатывать и выполнять какой либо функционал или даже возвращать значения.

Как?

Методы-расширения:

public class A { }
public static class AExt {
public static void SomeMethod(this A a) {
Console.WriteLine("Invoked with "+(a==null?"null":a.ToString()));
}
}


Это конечно хорошо, но имхо это нарушает "читабельность" - лучше явно проверять и обрабатывать null-значения, чем прятать обработку в методы расширения.

Но факт остается факт - у переменной со значением null можно вызвать метод :) Можно спрашивать на собеседованиях чтоб оценить реакцию :)

Saturday, April 24, 2010

C# 4.0: Небольшие малозаметные, но важные изменения

C# 4.0 вышел вместе с MSVS 2010 еще 12ого апреля. Уже довольно много статей о новых фичах вышло - о динамических типах и DLR, о параметрах и т.д. Но довольно много мелких изменений, который довольно слабо освещены, и о которых не стоит забывать.

Решил перечислить их с небольшими комментариями:


  • Во многие типы наконец-то добавили TryParse (TimeSpan, Enum, Guid).
  • Чуть проще работа со строками: Join'нить и Contact'ить можно все что IEnumerable, добавили String.IsNullOrWhiteSpace
  • Идея Enum'ов как флаги продолжила свою жизнь в виде Enum.HasFlag
  • Наконец-то не надо изобретать велосипед при копировании с одного потока в другой - System.IO.Stream.CopyTo
  • Теперь не надо писать конструкции типа:

    private MyType field;
    public Field { get { return field ?? (field = new MyType()); } }
    Вместо этого можно воспользоваться System.Lazy
  • Добавили System.Tuple<...> - вот этого часто не хватало. Определять свои классы лень только для того, чтоб вернуть два-три значения, а out-параметры не очень красивы.


Это в принципе именно полезные, но мало-заметные изменения. Полный список можно прочитать в этой статейке.

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


Tuesday, February 3, 2009

Функциональное программирование в “не функциональных” языках: ленивые вычисления

Сейчас модно говорить о ФП – что это такое, зачем и как его используют. Мне же хотелось бы рассматривать ФП как некоторый способ более эффективно реализовывать поставленную задачу. Т.е. не как просто новомодный (хотя ФП уже старо как мир) финт ушами, а как реальное практическое средство.

Один из элементов ФП, позволяющих эффективно его использовать являются “ленивые вычисления”. Принцип такой – зачем вычислять то, что возможно нам не понадобится.

Для того, чтоб лучше понять чем же это может быть удобно, я для себя составил вот такой иллюстрирующий пример:

Допустим у нас есть задача: написать функцию, принимающую три числовых параметра  (a, b, c), и возвращающую сумму a+b если a>0 и a+c если a<0.

Простейшая реализация (C#):

int function(int a, int b, int c) 
{
return a+a>0?b:c;
}


Но при вызове этой функции нам понадобится передать все три параметра. Но ведь реально нужны только 2 (в зависимости от значения первого или второго параметра). А если вычисление значений каждого из параметров является трудоёмка задача (выполнение запросов к базе данных, вычитка из большого документа или просто тяжело-вычисляемое значение), то нет смысла его делать.



Вот тут то и приходит на помощь идея ФП. А точнее то, что основным объектов является функция. Можно просто передать как параметр не значение, а функцию, которая возвращает это значение.



Таким образом, если мы перепишем эту функцию так, чтоб она оперировала не с цифрами, а с функциями, то получим, что при получении результата, будут вычисляться только те аргументы, которые реально нужны.



Теперь небольшой пример из совсем почти не функционального языка – C# 2.0:



Для начала перепишем функцию так, чтоб она оперировала с функциями (“делегатами”):



        private delegate int IntFunction();
private static IntFunction funct(IntFunction a, IntFunction b, IntFunction c)
{
return delegate
{
int aValue = a(); return aValue > 0 ? aValue + b() : aValue + c();
};
}


Далее, допустим есть функция,  которая очень долго работает. Для примера, пусть она возвращает число, переданное в качестве параметра:



        private static IntFunction CreateInt(int a)
{
return delegate
{
Console.WriteLine("Computing for long..long time, resulting "+a);
return a;
};
}


Ну а теперь сам вызов:



Console.WriteLine("Result is {0}",funct(CreateInt(1), CreateInt(2), CreateInt(3))());


В результате получим:



Computing for long..long time, resulting 1
Computing for long..long time, resulting 2
Result is 3


Как видно,  функция, возвращающая “3” даже не была запущенна.



Ну и само-собой, можно передавать результат выполнения функции как параметр для самой же функции, что позволяет выстраивать конструкции по типу:



Console.WriteLine("Result is {0}", funct(funct(CreateInt(1), CreateInt(2), CreateInt(3)), CreateInt(4), CreateInt(5))());


В результате которых будет  реально толь3о 3 раза вызвана наша “долгая” функция, вместо 5, как было бы при обычном подходе.



Вот такой простой и имхо понятный пример того, как можно понять смысл использования “ленивых вычислений”, и использовать в “не функциональных” языках.

Thursday, January 17, 2008

C#: 'foreach' or 'for'?

Некоторое время назад у меня с одним моим знакомым разработчиком был разговор, в котором появился вопрос о том, насколько эффективно использовать 'foreach' для итерации по простым массивам данных. Знакомый утверждал, что 'for' эффективней из-за того, что при 'foreach' в любом случае создаётся IEnumerator и вызывается MoveNext.

Решил провести небольшой тест и рассмотреть что же получается при компиляции двух функций:

        private static void Test(string[] test)
{
foreach (string s in test)
{
Console.WriteLine(s);
}
}
private static void Test2(string[] test)
{
for (int c = 0; c < test.Length; c++)
{
Console.WriteLine(test[c]);
}
}

 


В результате мы получаем:

.method private hidebysig static void Test(string[] test) cil managed
{
.maxstack 2
.locals init (
[0] string s,
[1] string[] CS$6$0000,
[2] int32 CS$7$0001)
L_0000: ldarg.0
L_0001: stloc.1
L_0002: ldc.i4.0
L_0003: stloc.2
L_0004: br.s L_0014
L_0006: ldloc.1
L_0007: ldloc.2
L_0008: ldelem.ref
L_0009: stloc.0
L_000a: ldloc.0
L_000b: call void [mscorlib]System.Console::WriteLine(string)
L_0010: ldloc.2
L_0011: ldc.i4.1
L_0012: add
L_0013: stloc.2
L_0014: ldloc.2
L_0015: ldloc.1
L_0016: ldlen
L_0017: conv.i4
L_0018: blt.s L_0006
L_001a: ret
}

.method private hidebysig static void Test2(string[] test) cil managed
{
.maxstack 2
.locals init (
[0] int32 c)
L_0000: ldc.i4.0
L_0001: stloc.0
L_0002: br.s L_0010
L_0004: ldarg.0
L_0005: ldloc.0
L_0006: ldelem.ref
L_0007: call void [mscorlib]System.Console::WriteLine(string)
L_000c: ldloc.0
L_000d: ldc.i4.1
L_000e: add
L_000f: stloc.0
L_0010: ldloc.0
L_0011: ldarg.0
L_0012: ldlen
L_0013: conv.i4
L_0014: blt.s L_0004
L_0016: ret
}

Внимательно проанализировав этот код, можно сделать вывод, что foreach фактически не добавил ничего, кроме объявления двух ссылок - одну для переменной 's', вторую для массива (при этом вторая ссылка на самом деле инициализируется значением ссылки аргумента - т.е. на исходных массив).


Таким образом для массивов использование foreach не несёт никаких дополнительных расходов.


Подчеркну, что это справедливо только для массивов - так как если заменить параметр на IEnumerable<string> (а Resharper "подсказывает" что это имеет смысл сделать), то получаем уже намного более "тяжелый" код:


 

.method private hidebysig static void Test3(class [mscorlib]System.Collections.Generic.IEnumerable`1<string> test) cil managed
{
.maxstack 1
.locals init (
[0] string s,
[1] class [mscorlib]System.Collections.Generic.IEnumerator`1<string> CS$5$0000)
L_0000: ldarg.0
L_0001: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<string>::GetEnumerator()
L_0006: stloc.1
L_0007: br.s L_0016
L_0009: ldloc.1
L_000a: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<string>::get_Current()
L_000f: stloc.0
L_0010: ldloc.0
L_0011: call void [mscorlib]System.Console::WriteLine(string)
L_0016: ldloc.1
L_0017: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
L_001c: brtrue.s L_0009
L_001e: leave.s L_002a
L_0020: ldloc.1
L_0021: brfalse.s L_0029
L_0023: ldloc.1
L_0024: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_0029: endfinally
L_002a: ret
.try L_0007 to L_0020 finally handler L_0020 to L_002a
}


Вывод: foreach использовать можно, но надо это делать акуратно :)

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 варианты некоторых пазлов можно посмотреть тут