Showing posts with label .NET od środka. Show all posts
Showing posts with label .NET od środka. Show all posts

08/01/2015

Czy sposób pisania pętli for ma wpływ na wydajność?

Home

Przy okazji codziennej prasówki natknąłem się na ten artykuł na temat wydajności pętli for w JavaScript'cie dla różnych przeglądarek. W skrócie chodzi o to czy powinniśmy pisać pętlą for tak:
for (int i = 0; i < array.Length; i++)
   ...
A może raczej tak, czyli zapamiętać długości tablicy w zmiennej pomocniczej i nie odczytywać jej przy każdym przebiegu pętli:
for (int i = 0, len = array.Length; i < len; i++)
   ...
Z ciekawości sprawdziłem czy taka mikro optymalizacja ma jakiekolwiek znaczenie w przypadku programowania na platformę .NET. Do zmierzenia czasu użyłem takiej metody pomocniczej:
public static void MeasureIt<T>(Action<T> action, T arg, int noOfIterations)
{
   var sw = new Stopwatch();
   sw.Start();

   for (var i = 0; i < noOfIterations; ++i)
      action(arg);

   sw.Stop();
   Console.WriteLine("Total time: " + sw.ElapsedMilliseconds);
}
Następnie wykonałem następujący test:
var noOfIterations = 100000;
var noOfElements = 10000;
var array = Enumerable.Repeat(1, noOfElements).ToArray();
//Przypadek 1
MeasureIt(arg =>
{
   var total = 0;
   for (var i = 0; i < arg.Length; i++)
      total += a[i];  
}, array, noOfIterations);
//Przypadek 2
MeasureIt(arg =>
{
   var total = 0;
   for (int i = 0, len = arg.Length; i < len; i++)
      total += a[i];
}, array, noOfIterations);
Dla rzetelności testy uruchomiłem wielokrotnie. Czas wykonywania obliczeń w obu przypadkach wynosił około 320ms z dokładnością do kilku milisekund. Czasami pierwsze podejście było szybsze, a czasami drugie. Z jednej strony czegoś takiego się spodziewałem. Z drugiej strony sądziłem, że jednak zaobserwuję pewien zysk w drugim przypadku. Dlaczego?

Otóż jeśli spojrzymy na kod pośredni wygenerowany przez kompilator to te dwie implementacje bynajmniej nie są identyczne. W pierwszym przypadku długość tablicy odczytywana jest wielokrotnie na koniec pętli, a w drugim tylko raz przed właściwą pętlą. Najwidoczniej jest to jednak tak szybka operacja, że się nie liczy (w IL do odczytania długości jednowymiarowej tablicy istnieje dedykowana instrukcja ldlen).

Dodatkowo przeprowadziłem podobny test dla użycia listy generycznej List<T>. Tym razem różnice były już zauważalne i wynosiły około 27% na rzecz zapamiętania liczby elementów listy w dodatkowej zmiennej. Średnio pierwsza wersja wykonywała się 957ms, a druga 752ms. Wynika to z tego, że aby odczytać liczbę elementów w liście należy odczytać właściwość Count czyli metodę get_Count. Na poziomie IL jest to robione przy pomocy instrukcji callvirt (w telegraficznym skrócie służącej do wołania metod na rzecz obiektów), a nie dedykowanej (i pewnie zoptymalizowanej) instrukcji ldlen jak ma to miejsce w przypadku tablic. Pomimo tych różnic uważam jednak, że w codziennej praktyce programistycznej nie należy się tym przejmować gdyż różnice w czasach obliczeń, w porównaniu do dużej liczby iteracji (100000), są zbyt małe.

02/12/2014

Zabawy z domenami aplikacyjnymi

Home

Proponuję zabawę z serii co zostanie wypisana na ekran. Mamy dwie klasy jak poniżej. Pierwsza z nich przekazywana jest pomiędzy domenami przez referencję, a druga przez wartość. Interfejs ITest ma charakter pomocniczy i nie ma znaczenia.
public interface ITest
{
   int Value { get; set; }
   void Start();
}

public class MarshalByRefObjectClass : MarshalByRefObject, ITest
{
   public int Value { get; set; }

   public void Start()
   {
      Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
      Value++;
   }
}

[Serializable]
public class MarshalByValueClass : ITest
{
   public int Value { get; set; }

   public void Start()
   {
      Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
      Value++;
   }
}
Mamy również następujący kod, w którym testuję jak zachowuja się:
  • Obiekty przekazywane przez wartość i przez referencję.
  • Utworzone w bieżącej (przy pomocy konstruktora) oraz w innej domenie (przy pomocy AppDomain.CreateInstanceFromAndUnwrap).
  • Przy wywołaniu na nich metody bezpośrednio oraz przy pomocy AppDomain.Callback.
Co daje łączenie 2 x 2 x 2 = 8 możliwości. Do testowania używam takiej metody pomocniczej:
private static void Test(AppDomain app, ITest test, bool doCallBack)
{
   if (doCallBack)
      app.DoCallBack(test.Start);
   else
      test.Start();

   Console.WriteLine(test.Value);
}
A to właściwy test, w którym najpierw tworzę obiekty przekazywane przez wartość/referencję lokalnie i w nowej domenie. Następnie wywołuję dla nich metodę Start i odczytuję właściwość Value.
var app = AppDomain.CreateDomain("TestDomain");

var asm = Assembly.GetExecutingAssembly();
var byRef = new MarshalByRefObjectClass();
var byRef1 = new MarshalByRefObjectClass();
var byRef2 = (MarshalByRefObjectClass)app.CreateInstanceFromAndUnwrap(asm.CodeBase, typeof(MarshalByRefObjectClass).FullName);
var byRef3 = (MarshalByRefObjectClass)app.CreateInstanceFromAndUnwrap(asm.CodeBase, typeof(MarshalByRefObjectClass).FullName);

var byValue = new MarshalByValueClass();
var byValue1 = new MarshalByValueClass();
var byValue2 = (MarshalByValueClass)app.CreateInstanceFromAndUnwrap(asm.CodeBase, typeof(MarshalByValueClass).FullName);
var byValue3 = (MarshalByValueClass)app.CreateInstanceFromAndUnwrap(asm.CodeBase, typeof(MarshalByValueClass).FullName);

Test(app, byRef, true);
Test(app, byRef1, false);
Test(app, byRef2, true);
Test(app, byRef3, false);

Test(app, byValue, true);
Test(app, byValue1, false);
Test(app, byValue2, true);
Test(app, byValue3, false);
Pytanie brzmi co zostanie wypisane na ekranie? Pokaż/Ukryj odpowiedź

Obiekty przekazywane przez referencję wypiszą na ekran nazwę domeny w jakiej zostały utworzone. Nawet jeśli wywołanie jest inicjowane w innej domenie to zostanie przekazne do domeny orginalej ponieważ pracujemy z proxy do obiektu. W przypadku obiektów przekazywanych przez wartość na ekran zostanie wypisana nazwa domeny w jakiej następuje wywołanie.

Jeśli chodzi o wartość wypisaną na ekran to obiekty przekazywane przez referencję zawsze wypiszą ten sam wynik = 1 ponieważ zarówno wywołanie metody Start jak i odczyt Value dotyczy tego samego obiektu. W przypadku obiektów przekazywanych przez wartość w niektórych przypadkach możemy otrzymać zero. Stanie się tak kiedy wywołanie Start nastąpi w innej domenie niż ta, w której odczytujemy właściwość Value. A dzieje sie tak ponieważ obie czynność dotyczą de facto innych obiektów.
Sandbox.vshost.exe
1
Sandbox.vshost.exe
1
TestDomain
1
TestDomain
1
TestDomain
0
Sandbox.vshost.exe
1
TestDomain
0
Sandbox.vshost.exe
1

11/11/2014

Domeny aplikacyjne i wątki

Home

Jakiś czas temu odpowiedziałem na pytanie na stackoverflow.com dotyczące kończenia pracy wątków z zewnętrznej biblioteki w sytuacji kiedy powinny już zakończyć pracę, a jednak tego nie zrobiły. Moja odpowiedź została co prawda skrytykowana, zresztą słusznie, ale dzięki temu dowiedziałem się rzeczy, która mi wcześniej umknęła.

W mojej odpowiedzi zasugerowałem, że skoro z jakiegoś powodu musimy użyć zewnętrznej biblioteki i ona nie działa to ja bym ją załadował do oddzielnej domeny aplikacyjnej i w tej nowej domenie uruchomił też obliczenia (wątki). Taką domeną można natomiast w dowolnym momencie "odładować" przy pomocy AppDomain.Unload. Napisałem nawet przykład pokazujący, że to działa. W czym więc problem?

Otóż wywołanie AppDomain.Unload może się nie powieść i zostanie wtedy rzucony wyjątku CannotUnloadAppDomainException. Powodów mogą być trzy, ale nas interesuje jeden. Kiedy wołamy AppDomain.Unload i wewnątrz domeny są aktywne jakieś wątki to AppDomain.Unload spróbuje je ubić przy pomocy metody Thread.Abort. Jeśli się uda to ok, ale Thread.Abort może nie być w stanie ubić wątku i wtedy zostanie wygenerowany wzmiankowany powyżej wyjątek.

Kiedy Thread.Abort nie zadziała? Dokumentacja wspomina o dwóch przypadkach.
  • Kiedy wątek wykonuje kod niezarządzany.
  • Kiedy wątek wykonuje właśnie kod wewnątrz bloku finally. To można bardzo łatwo symulować przez umieszczenie wywołania Thread.Sleep(100000) wewnątrz bloku finally.

21/09/2014

Od jakiej wartości zaczynają się indeksy tablic w C#?

Home

To post z serii ciekawostki. Większość z Was zapytana od jakiej wartości zaczynają się indeksy tablic w C# odpowie z pewnością, że od 0 i tego należy się trzymać, ale są pewne wyjątki. Oto przykład na jaki natknąłem się eksplorując czeluście platformy .NET pokazujący jak stworzyć tablicę 10x10 z indeksami zaczynającymi się od 5:
var array = Array.CreateInstance(typeof(int), new[] { 10, 10 }, new[] { 5, 5 });
var array2 = (int[,]) array;
A teraz mały przykład użycia:
array2[1,3] = 1 // Out of bounds array index
array2[5,6] = 1 // OK
array2[15,14] = 1 // Out of bounds array index
array2[14,14] = 1 // OK
Oczywiście nie byłbym sobą gdybym nie spróbował tego samego z tablicą jednowymiarową:
var array = Array.CreateInstance(typeof(int), new[] { 10 }, new[] { 5 });
var array2 = (int[]) array;
Taka próba rzutowania zakończy się niestety, a może na szczęście, wyjątkiem o treści:

Unable to cast object of type 'System.Int32[*]' to type 'System.Int32[]'.

Co oznacza zapis System.Int32[*]? Mówiąc szczerze nie jest do końca pewny. Bazując jednak na poniższym teście:
Console.WriteLine(new int[10].GetType()); 
Console.WriteLine(Array.CreateInstance(typeof(int), new[] { 10 }, new[] { 0 }).GetType());
Console.WriteLine(Array.CreateInstance(typeof(int), new[] { 10 }, new[] { 5 }).GetType()); 
Console.WriteLine(new int[10,10].GetType()); 
Console.WriteLine(Array.CreateInstance(typeof(int), new[] { 10, 10 }, new[] { 5, 5 }).GetType()); 
Który wypisze na ekran takie wyniki:

System.Int32[]
System.Int32[]
System.Int32[*]
System.Int32[,]
System.Int32[,]

Można stwierdzić, że nazwa typu TYP[*] oznacza po prostu tablicę jednowymiarową z indeksem nie zaczynającym się w zerze.

Na koniec jeszcze jedna ciekawostka. Rzutowanie System.Int32[*] na System.Int32[] w kodzie programu nie powiedzie się, ale już w oknie Quick Watch albo Immediate Window już tak:

05/01/2014

Jeszcze więcej szczegółów na temat IntelliTrace

Home

O IntelliTrace pisałem już wielokrotnie. Do tej pory nie wyjaśniłem jednak, że chociaż IntelliTrace nazywamy debugger'em historycznym to w rzeczywistości IntelliTrace jest profilerem. Dokładniej mówiąc jednym z komponentów składowych IntellITrace jest niezarządzana implementacja interfejsu ICorProfiler. Profiler ten komunikuje się z zarządzaną częścią IntelliTrace, czyli z programem IntellITrace.exe. IntellITrace.exe jest natomiast używane przez Visual Studio.

Ma to ciekawe skutki. Oznacza bowiem, że oprócz nagrywania działania aplikacji z poziomu Visual Studio albo bezpośrednio przy pomocy programu IntelliTrace.exe (jak to opisałem tutaj) dochodzi jeszcze jedna opcja. Otóż możemy skorzystać ze zmiennych środowiskowych COR_ENABLE_PROFILING oraz COR_PROFILER i monitorować przy pomocy IntelliTrace zarządzaną usługę systemową.

W tym celu znajdujemy w rejestrze klucz:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\<Nazwa_Usługi>

Dodajemy do niego nową wartość Environment o typie REG_MULTI_SZ i zawartości:
COR_PROFILER={b19f184a-cc62-4137-9a6f-af0f91730165}
COR_ENABLE_PROFILING=1
VSLOGGER_CPLAN=COLLECTION_PLAN_PATH
W pierwszeh linijce wskazujemy profiller, jaki ma zostać użyty do monitorowania usługi. W tym przypadku będzie to IntelliTrace dla VS 2012. Identyfikator profiler'a IntelliTrace dla VS 2010 jest inny tj. 301EC75B-AD5A-459C-A4C4-911C878FA196. Oczywiście, jeśli nie mamy zainstalowanej danej wersji Visual Studio, to profiler nie będzie zarejestrowany w systemie.

W drugiej linijce po prostu włączamy profilowanie, a w trzeciej wskazujemy ścieżkę to pliku XML z konfiguracją IntelliTrace. O tym, skąd wziąć ten plik, pisałem we wspomnianym już artykule oraz w innych postach z serii o IntelliTrace. Tutaj zaznaczę tylko, że należy w nim ustawić nazwę pliku z logiem oraz katalog roboczy np.:
<CollectionPlan xmlns="urn:schemas-microsoft-com:visualstudio:tracelog">
  <StartupInfo>
    <LogFileName>log.itrace</LogFileName>
    <LogFileDirectory>c:\Logs</LogFileDirectory>
    <MaximumLogFileSize>-1</MaximumLogFileSize>
  </StartupInfo>
  ...
</CollectionPlan>
Na koniec po prostu uruchamiamy naszą usługę, a kiedy wykona swoje zadanie zatrzymujemy i przeglądamy nagrany log na przykład w Visual Studio.

Niestety, ale to podejście nie zadziała dla zwykłych aplikacji uruchamianych z dwukliku. Sądzę, że w takich wypadkach IntelliTrace nie obsługuje zmiennej środowiskowej VSLOGGER_CPLAN, ale tego akurat nie jestem pewny. Istnieje jednak inna możliwość. W praktyce używana jest rzadko, gdyż jest mało wygodna, ale pokazuje jak IntelliTrace działa od środka. A więc uruchamiamy wiersz polecenia i wpisujemy następujące komendy:
rem Uruchamiamy instancję IntelliTrace o nazwie 'test' ale nie wskazujemy programu do monitorowania
%INTELLI_TRACE_PATH%\IntelliTrace.exe start /n:test /f:"D:\WorkingDir\IntelliTrace\IntelliTraceWorkingDir\Test.iTrace" /cp:%COLLECTION_PLAN%

rem Włączamy profilowanie
set COR_ENABLE_PROFILING=1

rem Ustawiamy profiler, który chcemy użyć do monitorowania naszego programu tj. IntelliTrace
set COR_PROFILER={b19f184a-cc62-4137-9a6f-af0f91730165}

rem Ustawiamy nazwę instancji IntelliTrace z jakiej chcemy skorzystać
set VSLOGGERNAME=test

rem Uruchamiamy program, który chcemy monitorować przy pomocy instancji IntelliTrace o nazwie 'test'
MyProgram.exe

rem Zamykamy instancję IntelliTrace o nazwie 'test'
%INTELLI_TRACE_PATH%\IntelliTrace.exe stop /n:test /cp:%COLLECTION_PLAN%
Podejście to różni się od standardowego tym, że tutaj instancja IntelliTrace czeka na uruchomienie programu, zamiast uruchomić go samemu.

07/11/2013

Metody rozszerzające w .NET 2.0

Home

.NET 2.0 to stara rzecz, ale wciąż z różnych powodów używana, na przykład dlatego, że klient nie chce zainstalować nowej wersji platformy na maszynach wszystkich użytkowników systemu. A co, jeśli pomimo tego wymarzy się nam użycie na przykład LINQ to Objects? Metody takie jak Select, Take itd. łatwo zaimplementować samemu, ale bez extensions methods ich użycie nie będzie takie przyjemne.

Zastanówmy się, co z tym robić. Metody rozszerzające obsługiwane są począwszy od .NET w wersji 3.5. Wiemy też, że mając kompilator dla .NET w wersji X możemy skompilować projekt dla .NET w wersji Y jeśli Y <= X. Do tego dodajmy, że extensions methods są mechanizmem czasu kompilacji i jako takie są niezależne od wersji platformy. Idąc tym tokiem rozumowania powinno być możliwe ich wykorzystanie w projektach używających .NET 2.0 o ile do kompilacji użyjemy nowszego kompilatora. Szybki eksperyment, czyli próba zdefiniowania metody rozszerzającej dla projektu używającego .NET 2.0 pokaże jednak, że coś jest nie tak i otrzymamy taki błąd kompilacji:

Cannot define a new extension method because the compiler required type 'System.Runtime.CompilerServices.ExtensionAttribute' cannot be found. Are you missing a reference?

Nie wszystko jednak stracone. Okazuje się, że wystarczy zdefiniować w projekcie następujący atrybut aby kompilacja zakończyła się powodzeniem:
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
    public sealed class ExtensionAttribute : Attribute { }
}
Trochę to brzydkie, ale możemy cieszyć się metodami rozszerzającymi w projektach korzystających ze starej wersji platformy. Atrybut ten jest potrzebny, ponieważ kompilator wykorzystuje go do oznaczenia metod tak, aby było wiadomo, które metody z różnych bibliotek są metodami rozszerzającymi.

Sztuczkę tą wykorzystują projekty takie jak LINQ for .NET 2.0 lub LinqBridge

12/09/2013

Nullable<T>.Equals(T value) 2

Home

Pytanie o opinię na temat Nullable<T>.Equals(T value) z poprzedniego postu zadałem również na portalu stackoverflow. Jeden z odpowiadających bardzo słusznie zwrócił uwagę, że opisany przeze mnie "problem" nie dotyczy tylko typu Nullable<T>. Aby przekonać się o czym mowa uruchomcie następujący kod:
Console.WriteLine((2m).Equals(2));
Console.WriteLine((2).Equals(2M));
Pierwsza myśli to 2xTrue ale poprawny wynik to True, False. Dzieje się tak ponieważ istnieje niejawna kowersja z typu int na decimal ale nie na odwrót. Czyli w pierwszym przypadku zostanie użyta metoda Equals(decimal value), a w drugim Equals(object value) A piszę o tym ponieważ to jedna z tych rzeczy, o których bardzo łatwo zapomnieć.

11/09/2013

Nullable<T>.Equals(T value)

Home

Po dłuższej urlopowej przerwie w blogowaniu zacznę od zagadki z serii co zostanie wypisane na ekran, którą podsunął mi kolega Przemek:
decimal d = 2;

Console.WriteLine("d == 2 = {0}", d == 2);
Console.WriteLine("d == (decimal)2 = {0}", d == (decimal)2);

Console.WriteLine("d.Equals(2) = {0}", d.Equals(2));
Console.WriteLine("d.Equals((decimal)2) = {0}", d.Equals((decimal)2));
Tutaj jeszcze nie ma haczyka i odpowiedź to 4XTrue. Zmieńmy jednak jedną liniję:

decimal? d = 2;

Tym razem odpowiedź jest mniej oczywista. Na ekran zostanie wypisane: True, True, False, True. Czym więc różni się pierwsze wywołanie Equals od drugiego?

W obu przypadkach wołana jest metoda wirtualna. W obu przypadkach metoda ta wołana jest dla struktury Nullable<T>. Zmienna d nie jest null'em, a więc to też nie jest problemem. Spójrzmy zatem jak zaimplementowano Equals dla Nullable<T>:
public override bool Equals(object other)
{
    if (!this.HasValue)
    {
        return (other == null);
    }
    if (other == null)
    {
        return false;
    }
    return this.value.Equals(other);
}
Nic skomplikowanego, jeśli zmienna jest ustawiona to ponownie wywoływana jest metoda Equals w tym przypadku Decimal.Equals Odpowiedzi musimy szukać więc dalej. Wszystkie typy numeryczne mają przeciążoną metodę Equals w następujący sposób:
public override bool Equals(object value)
public override bool Equals(decimal value)
Która z nich zostanie wywołana w tej sytuacji? Nullable<T>.Equals ma parametr typu object, a więc Decimal.Equals(object value) pasuje lepiej niż Decimal.Equals(decimal value). Ta pierwsza działa natomiast w ten sposób, że jeśli przekazany parametr nie jest typu decimal to zawsze zwraca false nie sprawdzając czy przekazany obiekt można bezpiecznie konwertować na decimal. I ot cała tajemnica ;)

Moim zdaniem działanie Nullable<T> nie jest teraz intuicyjne. Wzorując się na typach numerycznych, można by dopisać do Nullable<T> jeszcze jedną metodę:
public bool Equals(T other)
{
    if (!this.HasValue)
        return false;

    return this.value.Equals(other);
}
Z jakiegoś powodu tego nie zrobiono. Przeoczenie czy celowe działanie? Jestem ciekawy Waszych opinii.

22/03/2013

Konwencja wołania

Home

Konwencja wołania (ang. Calling convention) to zestaw zasad, który określa w jaki sposób metoda wołająca przekazuje parametry do metody wołanej i odbiera od niej wyniki. Programując w .NET, o ile nie współpracujemy z kodem natywnym, w ogóle nie musimy się tym interesować. Zgłębiając ten temat można jednak nauczyć się kilku ciekawych rzeczy.

Zacznijmy od tego jakiej konwencji wołania używa CLR. Na to pytanie można odpowiedzieć na dwa sposoby. Odpowiedź "wysoko poziomowa" brzmi: CLR został zaimplemntowany jako maszyna stosowa i nie używa rejestrów, a więc parametry oraz wyniki zostaną przekazane na stosie. Oto prosty przykład, zacznijmy od klasy:

public class Test
{
        public string ReturnString(int i, bool b)
        {
            return i.ToString() + b.ToString();
        }

        public string ReturnString(Fun f)
        {
            return f.ToString();
        }
}

Teraz spójrzmy na kod w C# i odpowiadający mu kod IL, w którym użyto tych metod:

var t = new Test();

newobj instance void ConsoleApplication1.Test::.ctor()
stloc.0 

var res1 = t.ReturnString(10, true);

ldloc.0 
ldc.i4.s 10
ldc.i4.1 
callvirt instance string ConsoleApplication1.Test::ReturnString(int32, bool)
stloc.1 

var res2 = t.ReturnString(new Fun());

ldloc.0
newobj instance void ConsoleApplication1.Fun::.ctor()
callvirt instance string ConsoleApplication1.Test::ReturnString(class ConsoleApplication1.Fun)
stloc.2 

Istotne fragmenty zaznaczyłem na czerwono. ldloc.0 odpowiada za wrzucenie na stos obiektu, na rzecz którego zostanie wywołana metoda (this). Potem wrzucamy argumenty dla wołanej metody czyli ldc.i4.s 10 oraz ldc.i4.1. Po wywołaniu metody zdejmujemy natomiast wynik ze stosu i zapisujemy w zmiennej lokalnej o indeksie 1 przy pomocy komendy stloc.1. Dla drugiego wywołania wygląda to podobnie. Polecenie newobj tworzy nowy obiekt i umieszcza referencję do niego na stosie.

Czemu jednak CLR został zaimplementowany jako automat stosowy (maszyna wirtualna Java zresztą też), a rejestry pojawiają się dopiero kiedy program jest wykonywany i IL jest zamieniany na kod natywny? Otóż CLR to komercyjna implementacja VES (ang. Virtual Execution System). VES jest częścią standardu ECMA-335, który jest niezależny od docelowej architektury, a w szczególności nie wspomina nic o żadnych rejestrach. Dzięki temu konkretne implementacji standardu mogą używać rejestrów w dowolny sposób.

I tutaj dochodzimy do odpowiedzi "nisko poziomowej" na postawione pytanie, czyli jakiej konwencji używa kod natywny wygenerowany na podstawie IL dla Windows'a. Otóż jest to konwencja fastcall, w której na przykład dla architektury x86 dwa pierwsze parametry przekazywane są w rejestrach ECX oraz EDX. Więcej szczegółów na ten temat można znaleźć na blogu Joe Duffy'ego.

Od siebie dodam, że jeśli korzystamy z P/Invoke to domyślnie metody natywne wołane są przy pomocy konwencji Winapi, która w systemie Windows jest tożsama z Stdcall. Zachowanie to możemy zmienić przy pomocy właściwości CallingConvention atrybutu DllImportAttribute.

29/10/2012

Trochę więcej o serializacji

Home

W poprzednim poście pisałem o serializacji XML'owej, a w tym wrócę jeszcze do tematu. Skorzystamy z tego samego kodu co poprzednio ale tym razem uruchomimy go korzystając z innych serializatorów. Zamieńmy, więc XmlSerializer na SoapFormatter lub BinaryFormatter i uruchommy przykładowy kod jeszcze raz (można użyć też DataContractSerializer ale z drobną modyfikacją kodu).

Efekt końcowy będzie inny niż dla serializatora XML'owego - po deserializacji właściwość Name będzie miała taką samą wartość jak przed serializacją. To nie jedyna różnica. Jeśli do konstruktora domyślnego dodamy linijkę:

Console.WriteLine("I'm in a constructor");

i popatrzymy na liczbę wypisanych na ekran napisów I'm in a constructor to okaże się, że w przypadku serializatora XML'owego konstruktor domyślny został wywołany dwa razy (przy tworzeniu obiektu oraz przy deserializacji), a w przypadku pozostałych klas tylko jeden raz (przy tworzeniu obiektu).

Należy postawić pytanie, jak w takim razie pozostałe serializatory tworzą obiekty przy deserializacji. Przecież muszą wołać jakiś konstruktor. Otóż nie koniecznie. O ile wiem taki BinaryFormatter, pozostałe serializatory korzystają zapewne z podobnych mechanizmów, używa metody FormatterServices.GetSafeUninitializedObject, która jest odpowiedzialna za zaalokowanie odpowiednio dużego fragmentu pamięci dla obiektu zadanego typu.

Po co to wszystko? Czy nie łatwiej wywołać konstruktor? Początkowo pomyślałem, że to kwestia wydajności. Ale szybki test pokazuje, że użycie FormatterServices.GetSafeUninitializedObject jest troszkę wolniejsze niż utworzenie obiektu za pomocą konstruktora.

var s = new Stopwatch();
s.Start();

for(int i = 0; i < 1000000; ++i)
 FormatterServices.GetSafeUninitializedObject(typeof(A));

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~130 ms

s.Reset();
s.Start();

for (int i = 0; i < 1000000; ++i)
    new A();

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~20 ms
Po chwili zastanowienia doszedłem do innego wniosku. Otóż, jeśli na proces deserializacji popatrzymy jak na proces "przywracania do życia" już raz utworzonych obiektów, to ponowne wywołanie konstruktora po prostu nie ma sensu albo wręcz jest niewskazane. Macie jakieś inne pomysły? Jak sądzicie czemu jedne serializatory wołają konstruktor, a inne nie?

27/10/2012

XmlSerializer- taka ciekawostka

Home

Serializator XML'owy platformy .NET jest bardzo łatwy i przyjemny w użyciu, ale czasami jego działanie może sprowadzić nas na manowce. Poniższy kod obrazuje o co mi chodzi. Zacznijmy od przykładowej, bardzo prostej klasy, którą będziemy serializować:

public class A
{
 public string Name { get; set; }
 
 public A()
 {
  Name = "Hello"; 
 }
}

Oraz kawałka kodu:

var obj = new A() { Name = null };

var stream = new MemoryStream();
var serializer = new XmlSerializer(typeof(A));

serializer.Serialize(stream, obj);

stream.Position = 0;

var obj2 = (A)serializer.Deserialize(stream);

Kod jest bardzo prosty. Tworzymy obiekt klasy A i ustawiamy właściwość Name na null. Następnie zapisujemy obiekt do strumienia. Po zapisaniu przesuwamy wskaźnik odczytu/zapisu strumienia na początek aby móc zdeserializować obiekt. W czym tkwi haczyk? Spróbujcie odpowiedzieć na to pytanie bez zaglądania do dalszej części posta:

Jaka będzie wartość właściwości Name po odczytaniu obiektu ze strumienia?

Otóż po zdeserializowaniu właściwość Name nie będzie pusta. Serializator XML'owy odczytując obiekt ze strumienia najpierw wywoła konstruktor domyślny. Potem zobaczy, że w strumieniu właściwość Name jest pusta i stwierdzi, że w takim wypadku nie trzeba niczego przypisywać do właściwości Name deserializowanego obiektu. W konstruktorze znajduje się natomiast kod, który ustawia tą właściwości. A więc po zdeserializowaniu właściwość Name będzie zawierała ciąg znaków "Hello", czyli inną niż przed serializacją.

Teraz wyobraźmy sobie trochę bardzie skomplikowaną sytuację. Załóżmy, że mamy kod pobierający jakieś dane z bazy danych i ładujący je do odpowiednich klas. Klasy te są napisane w podobny sposób jak klasa A powyżej, czyli w konstruktorze domyślnym niektóre właściwości są inicjowane wartościami innymi niż null. Dane (obiekty) są następnie udostępniane aplikacji klienckiej przez web service'y (stare dobre ASMX, usługi WCF jeśli użyto XmlSerializerFormatAttribute)  czyli muszą być najpierw zserializowane, a potem zdeserializowane. Kod działa poprawnie.

Teraz zabieramy się za pisanie testów integracyjnych i używamy tego samego kodu odpowiedzialnego za wczytywanie danych. W momencie, kiedy chcemy użyć tych samych obiektów (tak samo stworzonych i wypełnionych danymi) co aplikacja kliencka, nawet w ten sam sposób, okazuje się, że dostajemy wyjątek NullReferenceException.

Dlaczego? Ano dlatego, że w testach integracyjnych obiekty nie zostały przesłane przez sieć, czyli nie były serializowane i deserializowane, czyli konstruktor domyślny nie został wywołany drugi raz przy deserializacji i puste właściwości nie zostały ustawione tak jak opisano powyżej.

Jak w wielu innych podobnych sytuacjach, to żadna wiedza tajemna, ale jak się o tym nie wie, może napsuć trochę krwi. Pomijam tutaj fakt, że korzystanie z opisanej "funkcjonalności", świadomie czy nie, nie jest najlepszym pomysłem.

14/09/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy 2

Home

Niedawno kolega opowiedział mi o jeszcze jednym przypadku kiedy opisane przeze mnie zachowanie konstruktora statycznego w środowiskach x86/x64 doprowadziło do kłopotów. Scenariusz był dość ciekawy, dlatego go opiszę na uproszczonym przykładzie. Zacznijmy od tego, że napisaliśmy zarządzany komponent COM. Komponent ten w konstruktorze statycznym czyta wartość jakiegoś parametru konfiguracyjnego z pliku i na tej podstawie coś robi. W poniższym przykładzie, żeby nie komplikować sprawy, po prostu zapisuje jego wartości do pliku:

[ComVisible(true)]
public class MySimpleCOM : ServicedComponent
{
 static MySimpleCOM()
 {
  File.AppendAllText(
   @"c:\log.txt",
   String.Format("Value of configuration parameter = '{0}' in the domain '{1}'\n.", ConfigurationManager.AppSettings["Test"], AppDomain.CurrentDomain.FriendlyName));
 }
 
 public MySimpleCOM() {}
 public void Fun() {}
}

Dodam jeszcze, że do pliku AssemblyInfo.cs w projekcie z naszym zarządzanym komponentem zostały dodane takie atrybuty:

[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(false)]

ApplicationActivation zapewnia, że nasz komponent będzie hostowany poza procesem, w którym się do niego odwołamy, dokładniej przez proces dllhost.exe. Drugi atrybut jest opcjonalny ale ułatwia testy i mówi, że nie jest wymagana żadna kontrola dostępu (uprawnień) kiedy ktoś będzie próbował użyć naszego komponentu. Kod testujący nasz komponent jest jeszcze prostszy:

class Program
{
 static void Main(string[] args)
 {
  using (var host = new MySimpleCOM())
  {
   host.Fun();
  }
 }
}

Warto zwrócić uwagę, że nie trzeba samemu rejestrować komponentu. Zostanie to zrobione za nas.

Na koniec dodajemy jeszcze do pliku konfiguracyjnych programu testującego (app.config) oraz komponentu COM (Application.config) sekcję appSettings z parametrem test. Dla rozróżnienia niech wartość tego parametru różni się w obu przypadkach np.:

<appSettings>
 <add key="test" value="Tester"/>
</appSettings>

oraz

<appSettings>
 <add key="test" value="COM"/>
</appSettings>

Aby zobaczyć na czym dokładnie polega problem najłatwiej postąpić w następujący sposób. Najpierw kompilujemy oba projekty z opcją x86, uruchamiamy i zaglądamy do pliku z log.txt. Dalej kompilujemy ponownie oba projekty z opcją x64 i znowu uruchamiamy pamiętając aby uprzednio odinstalować wersję x86 komponent COM. Można to zrobić na przykład przy pomocy narzędzia Usługi składowe (dcomcnfg) wbudowanego w system Windows.

W pierwrzym przypadku (x86) plik log.txt będzie wyglądał tak:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.

A w drugim (x64) w następujący sposób:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.
Value of configuration parameter = 'Tester' in the domain 'Tester.vshost.exe'.

Jak widać w przypadku środowiska x64 kod inicjalizujący w konstruktorze statycznym został wywołany dwa razy, do tego z różnymi wartościami parametru konfiguracyjnego. W prawdziwym systemie mogłoby to doprowadzić do powstania jakichś błędów i chyba sami przyznacie, że ich przyczyna nie byłaby trywialna do wykrycia.

PS.

Jeśli przy własnoręcznym odtwarzaniu tego scenariusza napotkacie problem, że komponent COM nie będzie czytał konfiguracji ze pliku Application.config, to odsyłam na przykład do tego wpisu, w którym wszystko jest wyjaśnione.

05/06/2012

CLR Profiling API - dobrego złe początki

Home

Jakiś czas temu odpowiadając na komentarz pod postem przebąknąłem, że przymierzam się do wzięcia byka za rogi i chcę napisać własny profiler korzystając z CLR Profiling API. Zagadnienie nie jest trywialne dlatego postanowiłem zacząć od zapoznania się z "literaturą". Na pierwszy ogień poszedł artykuł Write Profilers With Ease Using High-Level Wrapper Classes. Swoje lata ma, chociaż na liście mam jeszcze starsze artykuly, ale akurat w przypadku CLR  Profiling API lata biegną wolniej niż w przypadku innych technologii.

Do artykułu, jakby inaczej, dołączony jest kod, który postanowiłem jak najszybciej uruchomić aby zobaczyć czy to rzeczywiście działa. W tym momencie natknąłem się na pierwszy problem i o tym będzie ten post. Problem wynikał z tego, że chciałem "sprofilować" aplikację używającą .NET 4.0 kodem napisanym z myślą o .NET 2.0. Z biegu nie jest to możliwe i objawia się takim błędem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed. The profiler that was configured to load was designed for an older version of the CLR. You can use the COMPLUS_ProfAPI_ProfilerCompatibilitySetting environment variable to allow older profilers to be loaded by the current version of the CLR. Please consult the documentation for information on how to use this environment variable, and the risks associated with it. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. Process ID (decimal): 5128. Message ID: [0x2517].


Łatwo jednak sobie z tym poradzić, wystarczy ustawić zmienną środowiskową COMPLUS_ProfAPI_ProfilerCompatibilitySetting. Uruchomienie programu pod kontrolą profiler'a będzie więc wyglądało w następujący sposób:

rem Rejestrujemy dll'kę z profiler'em
regsvr32 /s MyProfiler.dll

rem Włączenie profilowania
set COR_ENABLE_PROFILING=1

rem Ustawiamy profiler, który chcemy użyć (zarejestrowaliśmy go 2 linijki wcześniej)
set COR_PROFILER={32E2F4DA-1BEA-47ea-88F9-C5DAF691C94A}

rem Włączamy kombatybilność wstecz
COMPLUS_ProfAPI_ProfilerCompatibilitySetting=EnableV2Profiler

rem Uruchamiamy program
MyProgram.exe

rem Profiler nie jest już potrzebny, więc go wyrejestrowujemy
regsvr32 /s /u MyProfiler.dll

Ale to nie wszystko. Drugi rodzaj błędu z jakim się spotkałem objawia się następującym wpisem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed during CoCreateInstance. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. HRESULT: 0x80070002. Process ID (decimal): 2620. Message ID: [0x2504].

I może wynikać z dwóch rzeczy. Po pierwsze jeśli nasz program jest programem 64 bitowym, do jego profilowania musimy użyć 64 bitowej wersji profilera. Sprawa ma się podobnie, jeśli program jest 32 bitowy. W przeciwnym wypadku środowisko nię będzie potrafiło załadować odpowiedniej biblioteki. Błąd pojawi się również jeśli spróbujemy uruchomić profiler nie posiadając praw administratora (Windows Vista z włączonym UAC). A więc uruchamiając skrypt należy skorzystać z opcji Uruchom jako administrator.

09/05/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy

Home

Na początku marca pisałem o "dziwnym" zachowaniu konstruktora statycznego. W skrócie chodziło o to, że jeśli chcieliśmy wywołać metodę w domenie aplikacyjnej innej niż domyślna, konstruktor statyczny klasy, do której należała ta metoda, wołany był o jeden raz więcej w środowisku x64 niż w środowisku x86. Ponieważ sprawa mnie nurtowała postanowiłem zadać pytanie poprzez portal Microsoft Connect. Zajęło to trochę ale w końcu uzyskałem wyjaśnienie. Odpowiedź jest dość ciekawa, a więc zapraszam do lektury.

03/03/2012

Domeny aplikacyjne, konstruktor statyczny, a platforma x86 vs x64

Home

Na początek trochę kodu. Zacznijmy od klasy testowej:
public class TestClass : MarshalByRefObject
{
    static TestClass()
    {
        Console.WriteLine(String.Format("I'm in the static constructor in the domain '{0}'.",
            AppDomain.CurrentDomain.FriendlyName));
    }

    public void Hello()
    {
        Console.WriteLine(String.Format("Hello from the domain '{0}'.", AppDomain.CurrentDomain.FriendlyName));
    }
}
Teraz kod testujący:
AppDomain domain = AppDomain.CreateDomain("Test");

TestClass t = (TestClass)domain.CreateInstanceAndUnwrap(typeof(TestClass).Assembly.FullName, typeof(TestClass).FullName);
t.Hello();
Oraz pytanie co zostanie wypisane na ekran? A w szczególności ile razy zostanie wywołany konstruktor statyczny? W głównej domenie aplikacyjnej? W domenie pomocniczej? A może w obu?

Skoro o to pytam to zapewne gdzieś tkwi haczyk. Otóż okazuje się, że wynik będzie zależał od tego czy program został skompilowany z opcję Platform target ustawioną na x86 czy x64. W przypadku x86 na ekran zostanie wypisany taki wynik:

I'm in the static constructor in the domain 'Test'.
Hello from the domain 'Test'
A w przypadku x64 taki:
I'm in the static constructor in the domain 'Test'.
I'm in the static constructor in the domain 'ConsoleApplication.vshost.exe'.
Hello from the domain 'Test'
Jeśli natomiast program zostanie skompilowany z opcją AnyCPU wynik będzie zależał od maszyny na jakiej go uruchomimy.

Konstruktor statyczny dla danej klasy wołany jest co najwyżej jeden raz w danej domenie aplikacyjnej. Jak jednak widać w zależności od platformy może zostać wywołany w jednej lub dwóch domenach. Może to mieć znaczenie kiedy jego kod będzie zawierał np.: jakiś kod inicjalizujący. Przedstawiony scenariusz nie jest zbyt częsty ale jeśli wystąpi, wykrycie błędu może być trudne, dlatego dobrze wiedzieć o tej różnicy.

Opisane zachowanie testowałem na trzech maszynach więc zakładam, że nie jest to coś lokalnego i przypadkowego.

28/10/2011

Wiele usług w jednym procesie 2

Home

W poście tym wrócę jeszcze do tematu uruchamiania kilku usług w jednym procesie. Otóż, ciekawe jest to, że można konfigurować to zachowanie już po zainstalowaniu usługi. Służy do tego, i nie tylko tego, program wiersza poleceń o nazwie sc. Poniżej przedstawiam przykład jego użycia.

Zacznijmy od pobrania konfiguracji usługi ABC przy pomocy komendy sc query ABC. Przykładowy wynik pokazałem poniżej.
SERVICE_NAME: ABC
TYPE               : 20  WIN32_SHARE_PROCESS
STATE              : 1  STOPPED
  (NOT_STOPPABLE,NOT_PAUSABLE,IGNORES_SHUTDOWN)
WIN32_EXIT_CODE    : 1077       (0x435)
SERVICE_EXIT_CODE  : 0  (0x0)
CHECKPOINT         : 0x0
WAIT_HINT          : 0x0
Jak widać usługa ABC może współdzielić proces z jakąś inną usługą. Efekt działania bardzo ładnie widać w menadżerze zadań. Jeśli uruchomimy usługi: ABC oraz ABCDebug to menadżer zadań pokaże tylko jeden proces.



Aby to zmienić należy użyć komendy sc config ABC type= own. Teraz, po uruchomieniu obu usług, menadżer zadań pokaże dwa procesy.



Do stanu pierwotnego wracamy przy pomocy komendy sc config ABC type= share. Warto zwrócić uwagę na jeszcze jedną rzecz. Spójrzmy na ten scenariusz.
  • Mamy dwie usługi współdzielące proces: ABC oraz ABCDebug..
  • Uruchamiamy obie i menadżer zadań pokazuje jeden nowy proces: WindowsService1.exe.
  • Zatrzymujemy usługę ABC.
  • Usługa ABCDebug nadal działa.
  • sc config ABC type= own
  • Uruchamiamy usługę ABC i teraz menadżer zadań pokazuje dwa procesy WindowsService1.exe.
  • Zatrzymujemy usługę ABC
  • sc config ABC type= share
  • Ponownie uruchamiamy usługę ABC i menadżer zadań znowu pokazuje jeden proces WindowsService1.exe.
Czyli w czasie kiedy zmienialiśmy konfigurację usługi ABC usługa ABCDebug cały czas działała i w niczym to nie przeszkadzało.

26/10/2011

Wiele usług w jednym procesie

Home

Istnieje kilka podejść do debugowania usług systemowych. Jeśli chcemy debugować już uruchomioną usługę to możemy skorzystać z opcji Attach to process.... Sprawa jest trudniejsza jeśli chcemy podłączyć się do usługi w momencie jej uruchamiania. W takim wypadku można w kodzie usługi wywołać metodę Debugger.Brake. Są też inne sposoby, na przykład sztuczne opóźnienie startu usługi, tak aby zdążyć się do niej podpiąć.

Ostatnio poznałem nowe, bardzo ciekawe podejście. Polega ono na stworzeniu dodatkowej, pomocniczej "pustej" usługi i zainstalowaniu jej w odpowiedni sposób razem z właściwą usługą. Strukturę przykładowego projektu widać na poniższym rysunku.



Projekt musi również zawierać dwa instalatory (klasa ServiceInstaller), po jednym dla każdej z usług.



W kodzie przekłada się to na coś takiego.
...
this.Installers.AddRange(new System.Configuration.Install.Installer[] {
  this.serviceProcessInstaller1,
  this.serviceInstaller1,
  this.serviceInstaller2});
...
Instalację przeprowadzamy standardowo przy pomocy narzędzia InstallUtil. Zadba ono o to aby zainstalować obie usługi za jednym razem. Następnie, przy debugowaniu najpierw uruchamiamy tą drugą, dodatkową usługę, podczepiamy się do niej przy pomocy opcji Attach to process..., stawiamy pułapkę w kodzie pierwszej usługi i dopiero ją uruchamiamy. To zadziała ponieważ obie usługi zostaną uruchomione w jednym procesie, a co więcej otrzymujemy to za darmo. Odpowiada za to metoda ServiceIntaller.Install, której fragment przytaczam poniżej.
int serviceType = 0x10;
...
if (numberOfServices > 1)
{
  serviceType = 0x20;
}
...
NativeMethods.CreateService(databaseHandle, this.ServiceName, this.DisplayName, 0xf01ff, serviceType, (int) this.StartType, 1, str3, null, IntPtr.Zero, dependencies, servicesStartName, password);
...
CreateService to natywna metoda WinAPI, która dodaje usługę do bazy danych menadżera usług. Istotny jest jej piąty parametr serviceType, może przyjąć kilka wartości ale nas interesują dwie:
  • SERVICE_WIN32_OWN_PROCESS = 0x00000010 oznacza, że każda usługa działa w swoim procesie
  • SERVICE_WIN32_SHARE_PROCESS = 0x00000020 oznacza, że usługi mogą dzielić jeden proces
Jeśli liczba usług jest większa niż 1 to ServiceIntaller.Install używa flagi SERVICE_WIN32_SHARE_PROCESS, a w przeciwnym wypadku SERVICE_WIN32_OWN_PROCESS.

03/05/2011

The Controls collection cannot be modified because the control contains code blocks (i.e. % ... %)

Home

The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>). to paskudny błąd, który generowany jest przez klasę System.Web.UI.ControlCollection. Ma to miejsce przy próbie modyfikacji kolekcji, na przykład przez wywołanie Add(Control control), w przypadku kiedy kontrolki w niej zawarte korzystają z wyrażeń postaci <% ... %> lub <%= ... %>. Istnieje kilka sposób ominięcia tego problemu:
  • Kod zawierający wyrażenia <% ... %> lub <%= ... %> umieścić w dodatkowym bloku <div runat="server">...<div/>. Na przykład taki kod:
    <form id="form1" runat="server">
        <% for (int i = 0; i < 5; ++i) %>
        <% { %>
        <br />
        <asp:label runat="server" Text="Test" />
        <%= i.ToString() %>
        <% } %>
    </form>
    
    zamienimy na taki:
    <form id="form1" runat="server">
        <div runat="server">
        <% for (int i = 0; i < 5; ++i) %>
        <% { %>
        <br />
        <asp:label runat="server" Text="Test" />
        <%= i.ToString() %>
        <% } %>
        </div>
    </form>
    
    W drugim przypadku modyfikacja kolekcji (form1.Controls.Add(new Label());) nie spowoduje błędu ponieważ kontrolki form oraz div posiadają oddzielne kolekcje ControlCollection i tylko kolekcja kontrolki div jest w trybie tylko do odczytu.
  • Jeśli mamy dostęp do kontrolek Telerika to możemy skorzystać z RadCodeBlock.
Poniżej jeszcze dwa, mniej ogólne, rozwiązania problemu:
  • Zastąpienie <%= ... %> przez <%# ... %>. Na przykład taki kod:
    <%= Environment.MachineName %>
    
    możemy zastąpić takim:
    <%# Environment.MachineName %>
    
  • Użyć kontrolki Literal i taki kod:
    <%= Resources.lbl.Title %>
    
    zastąpić takim:
    <asp:Literal runat="server" Text="<%$ Resources: lbl, Title %>"/>
    
Rozwiązanie problemu to jedna sprawa. Mnie zaciekawiło dlaczego tak się dzieje. Poszukiwania rozpocząłem od przyjrzenia się implementacji kolekcji ControlCollection. Tutaj jak zwykle pomocny okazał się .NET Reflector. Na początki dowiedziałem się, że kolekcja ma prywatne pole _readOnlyErrorMsg; . Jeśli jest ono różne od null i nastąpi próba modyfikacji kolekcji to generowany jest wyjątek:
...
if (this._readOnlyErrorMsg != null)
{
  throw new HttpException(SR.GetString(this._readOnlyErrorMsg));
}
...
Pole to ustawiane jest tylko w jednym miejscu, w metodzie ControlCollection.SetCollectionReadOnly(string errorMsg). Metoda ta wywoływana jest w kilku miejscach, ale tylko w jednym przypadku parametr errorMsg odpowiada komunikatowi The Controls collection cannot be modified.... Ma to miejsce w metodzie Control.SetRenderMethodDelegate(RenderMethod renderMethod). I tu pojawił się problem. Okazało się, że ta metoda nie jest nigdzie wywoływana, a przynajmniej tak twierdził .NET Reflector (i w sumie się nie pomylił).

Wywołanie tej metody znalazłem w plikach generowanych dynamicznie dla stron aspx. Znajdziemy je w katalogu z plikami tymczasowymi C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\.... Dokładej lokalizacji nie podaję ponieważ nazwy tych plików generowane są w sposób losowy. Dla przykładu z początku postu (z pętlą for) zostanie wygenerowany taki kod:
...
@__ctrl.SetRenderMethodDelegate(new System.Web.UI.RenderMethod(this.@__Renderform1));
...
Metoda @__Renderform1 wygląda natomiast tak (po usunięciu dyrektyw kompilatora):
...
private void @__Renderform1(System.Web.UI.HtmlTextWriter @__w, System.Web.UI.Control parameterContainer) 
{
  @__w.Write("\r\n    \r\n        ");
  for (int i = 0; i < 5; ++i) 
  { 
    @__w.Write("\r\n    <br />\r\n    ");
    parameterContainer.Controls[0].RenderControl(@__w);
    @__w.Write("\r\n    ");
    @__w.Write( i.ToString() );
    @__w.Write("\r\n    ");
  }
}
...
Co w tym takiego zdrożnego, że silnik ASP.NET nie pozwala na modyfikowanie kolekcji? Moim zdaniem kluczowa jest linijka zawierająca odwołanie to kolekcji Controls. Jak widać indeks kontrolki do jakiej się odwołujemy jest ustawiony na sztywno w kodzie. W takim przypadku modyfikowanie kolekcji kontrolek byłoby albo bezcelowe bo i tak kontrolka nie została by wyrenderowana albo doprowadziło to wyrenderowania innej kontrolki niż trzeba.

Ciekawe posty na ten temat można znaleźć również na blogu Rick Strahl's Web Blog. Podaje tylko tytuły artykułów, a nie linki ponieważ w momencie pisania tego postu artykuły te były dostępne tylko w cache Google'a:
  • Reliably adding Controls or Markup before or after an ASP.NET Control?
  • Understanding how <% %> expressions render and why Controls.Add() doesn't work

17/02/2009

operator==

Home

Pomysł na napisanie tego posta podsunął mi kolega z pracy. Na początek fragment klasy, która we wzorcowy sposób dostarcza własnej implementacji operatora równości:
public class Test
{
  private int i;
  
  public Test(int i)
  {
    this.i = i;
  }

  public static bool operator==(Test a, Test b)
  {
    if (System.Object.ReferenceEquals(a, b))
      return true;

    if (((object)a == null) || ((object)b == null))
      return false;

    return a.i == b.i;
  }
  
  ...
}
Oczywiście potrzebna jest jeszcze implementacja operatora nierówności !=. Chciałbym też zwrócić uwagę, ponieważ sam zapomniałem o tym pisząc ten przykład :), że implementując operator równości (czy też nierówności) kiedy wykonujemy porównanie takie jak a == b, a == null czy b == null musimy zrzutować testowane obiekty do typu object. W innym wypadku nastąpi rekurencyjne wywołanie naszej implementacji operatora. Rzutowanie zapewni, że zostanie wywołana implementacja bazowa - operatory nie działają jak metody wirtualne. Inne rozwiązanie to użycie metody System.Object.ReferenceEquals. Teraz popatrzmy na kod poniżej i odpowiedzmy sobie na pytanie jaką wartość przyjmą zmienne lokalne res oraz res2:
public class Program
{
  public static bool MyEquals(T a, T b) where T : class
  {
    return a == b;
  }
  
  public static void Main(string[] args)
  {
    Test a = new Test(1);
    Test b = new Test(1);

    bool res = a == b;
    bool res2 = MyEquals(a, b);
  }
}
Odpowiedź brzmi oczywiście true oraz false. Piszę o tym również dla siebie ponieważ sam musiałem się zastanowić jaki będzie wynik. Pierwszy rezultat jest chyba oczywisty i nie wymaga żadnych tłumaczeń. Po prostu wywołany zostanie zaimplementowany przez nas operator. A co z wywołaniem metody generycznej MyEquals. Może się wydawać, że skoro jest to metoda generyczna to zostanie wywołany operator równości dla typu przekazanego jako argument typu generycznego (ang. generic type parameters). Argument typu generycznego to to co podstawiamy pod T czyli w tym przypadku Test.

Jest jednak inaczej. W momencie kompilacji kompilator nie wie co zostanie podstawione pod T. W związku z tym generowany jest kod sprawdzający równość referencyjną. Na poziomie IL oznacza to użycie instrukcji ceq. Jeśli zmienimy kod metody MyEquals tak jak poniżej kompilator będzie już mógł wybrać operator równości dostarczony przez nas zamiast bazowego.
public static bool MyEquals(T a, T b) where T : Test
{
  return a == b;
}

15/12/2008

Domknięcie - Jak to działa?

Home

Domknięcie (ang. Closure) to cecha języków programowania, która pozwala funkcji odwołać się to zmiennych zdefiniowanych poza jej ciałem. Domknięciem nazywa się również obiekt (byt) wiążący funkcję z środowiskiem jej wykonania. Oczywiście chodzi o zmienne inne niż globalne. Takie powiązane zmienne w języku angielskim określane są mianem bound variables. Przykład domknięcia został przedstawiony poniżej. Proponuję przyjrzeć się temu kodowi i odpowiedzieć co zostanie wypisane na ekran. W dalszej części posta będę używał terminu metoda zamiast funkcja, który bardziej pasuje do obiektowego języka jakim jest C#.
delegate void Fun();
...
List array = new List();
for (int i = 0; i < 5; ++i)
{
  array.Add(delegate() { Console.WriteLine(i); });
}

for (int i = 0; i < 5; ++i)
{
  array[i]();
}
Implementacja domknięcia zależy od konkretnego języka programowania. Mnie oczywiście zaciekawiło jak to zostało zrealizowane w C# i tym zajmę sie w tym poście. W rozwiązaniu zagadki pomógł oczywiście reflektor Lutz Roeder’s Reflector, o którym wspominałem już wcześniej.

Implementacja domknięcia w języku C# bazuje na automatycznie generowanej przez kompilator klasie. Klasa ta zawiera kod metody oraz powiązane z nią zmienne. Dla naszego przykładu klasa ta będzie wyglądać jak poniżej:
[CompilerGenerated]
private sealed class DisplayClass
{
  public int i;

  public void b_0()
  {
    Console.WriteLine(this.i);
  }
}
Kod zmieniłem tak aby był bardziej czytelny. Jak widać wygenerowana klasa jest trywialna. Zawiera jedną metodę, której zawartości odpowiada zawartości zdefiniowanej przez nas anonimowej metody oraz jedno publiczne pole, które stanowi nic innego jak powiązaną zmienną. No dobrze, ale kto korzysta z wygenerowanej klasy i kto ustawia wartość publicznego pola i. Odpowiedź nasuwa się sama - oczywiście kompilator. A jak? Zostało to schematycznie przedstawione poniżej:
DisplayClass d = new DisplayClass();

List array = new List();
for (d.i = 0; d.i < 5; ++d.i)
{
  array.Add(d.b_0);
}

for (int i = 0; i < 5; ++i)
{
  array[i]();
}
Kod ten może trochę dziwić. Od razu nasuwa się pytanie, czemu utworzono tylko jedną instancję automatycznie wygenerowanej klasy? Wszystko się jednak zgadza. Domknięcie działa w ten sposób, że anonimowa metoda ma dostęp do wartości zmiennej powiązanej z czasu jej wykonywania, a nie z czasu kiedy została utworzona. Nie trudno zauważyć, że w momencie wywoływania metody wartość zmiennej i wynosi 5. Odpowiedź na postawione przeze mnie wcześniej pytanie brzmi, więc: Na ekran zostanie wypisany ciąg: 5 5 5 5 5. Poniżej podaję sposób w jaki osiągnąć bardziej intuicyjny efekt wypisania na ekran ciągu: 0 1 2 3 4. Należy tylko zmodyfikować pierwszą pętlę:
for (int i = 0; i < 5; ++i)
{
  int j = i;
  array.Add(delegate() { Console.WriteLine(j); });
}
Dla takiego przypadku kod wygenerowany przez kompilator będzie już inny. W szczególności zostanie utworzonych 5 instancji klasy DisplayClass, a nie jedna jak w poprzednim wypadku.

Przykład omówiony przeze mnie jest bardzo prosty. W bardziej skomplikowanych przypadkach sprawa nie jest już tak prosta ale koncepcja implementacji domknięcia pozostaje taka sama.