2009-06-05

DebuggerHiddenAttribute, DebuggerNonUserCodeAttribute, DebuggerStepThrough

Wszystkie trzy atrybuty najlepiej omówić razem, bo różnice pomiędzy nimi są subtelne. Atrybuty te posiadają konstruktory bezargumentowe. Atrybuty te nie podlegają dziedziczeniu. Najpierw powiedzmy sobie o opcji Just My Code. Opcja ta wpływa na działania wspomnianych trzech atrybutów. Ustawiamy ją w opcjach Visuala (Tools|Options): Wszystkie trzy atrybuty modyfikują zachowania debugera w następujących kwestiach:
  • czy debuger zatrzymuje się na breakpoincie w bloku kodu oznaczonym jednym z podanych atrybutów
  • czy debuger wykonuje Step Into w blok kodu oznaczony jednym z tych atrybutów
  • jak w podglądzie Call Stack pokazywane są wywołania metod oznaczonych tym atrybutem
Poniższa tabelka prezentuje różnice pomiędzy tymi trzema atrybutami:
AtrybutDebuggerHiddenDebuggerNonUserCodeDebuggerStepThrough
Just My CodeOnOffOnOffOnOff
BreakpointIgnorowanyIgnorowanyIgnorowanyDziałaIgnorowanyDziała
Step IntoOmijaOmijaOmijaDziałaOmijaOmija
Call StackBrak wpisuBrak wpisuExternal CodeJest wpisExternal CodeJest wpis
ZastosowanieProperty
Constructor
Method
Class
Struct
Property
Constructor
Method
Class
Struct
Property
Constructor
Method
Jeśli atrybutem objęta jest cała klasa lub struktura to atrybut ten stosuje się do wszystkich elementów klasy lub struktury. Choć atrybuty te można stosować do property to nie odniesie to żadnego skutku:
[System.Diagnostics.DebuggerNonUserCode()]
public int Z
{
    get
    {
        return 6;
    }
}
Zamiast tego atrybut należy stosować osobno dla gettera i settera. Atrybuty, które można stosować dla konstruktorów, można także stosować dla konstruktorów statycznych. Jeśli dokonujemy Step Into w kod oznaczony jednym z podanych atrybutów (i Just My Code jest tak ustawione, że z zgodnie z tabelką ten blok kodu zostanie przeskoczony), a z bloku tego wywoływane są inne bloki kodu nie oznaczone żadnym z powyższych 3 atrybutów, to właśnie na tamtym bloku zatrzyma się debuger (niezależnie od kombinacji Just My Code i atrybutów). To jak przeskoczona metoda zostanie pokazana w Call Stack podano w tabelce. Weźmy taki przykład, dla włączone Just My Code
[System.Diagnostics.DebuggerStepThrough()]
public int TestM()
{
    return TestMM();
}

public int TestMM()
{
    return 6;
}
Po wstawieniu breakpointa na zaznaczoną linię w Call Stack zobaczymy: To ukrycie w śladzie wywołania dotyczy tylko debugera, ślad stosu uzyskany z wyjątku lub za pomocą Console.WriteLine(Environment.StackTrace) będzie zawierać metodę TestM() Kiedy wyłączymy Just My Code, Call Stack będzie wyglądał tak: Tutaj, też widzimy zastosowanie tej opcji, polegające na ukrywaniu przed nami, wszelkich śladów wywołań, które nie dotyczą naszego kodu. Więcej informacji dostępnych jest tutaj. No i możliwa jest jeszcze taka kombinacja atrybutu i Just My Code, że ślad pomijanej metody nie będzie wogóle pokazywany: Atrybuty te powinniśmy stosować tylko dla dobrze przetestowanego kodu, by podczas debugowania nie szukać błędu i miejsca z błędem nie przeskakiwać. Można szybko docenić jego zalety implementując np. klasę liczby zespolonej, punktu, prostokąta, gdzie może być dużo małych upierdliwych metod, operatorów, konstruktorów wykonywanych setki razy.

2009-06-02

System.Diagnostic.DebuggerDisplayAttribute

Za pomocą tego atrybutu kontrolujemy jak środowisko debugujące (Visual Studio) wyświetla nam informacje o stanie zmiennych (pól i właściwości) podczas debugowania. Reguły definiowane przez ten atrybut dotyczą zarówno okienek wyskakujących po najechaniu na zmienną, jak i tego co pojawia się w Watch, Autos, Locals. Atrybut ten może zostać nałożony na bardzo wiele elementów, poniżej wyciąg z źródeł: [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Delegate, AllowMultiple = true)] Klasa, struktura Rozpatrzmy najpierw użycie tego atrybuty na klasie (na strukturze wygląda identycznie):
class TestClass1
{
   [System.Diagnostics.DebuggerBrowsable( System.Diagnostics. DebuggerBrowsableState.RootHidden)]
   public int[] Collection
   {
       get
       {
      
           return new int[] { 1, 2, 3, 4 };
       }
   }
}
Obiekt tej klasy w podglądzie będzie wyglądać następująco: Jeśli klase tą oznaczymy atrybutem: [System.Diagnostics.DebuggerDisplay("Count = {Collection.Length}")] , to jej wygląd w podglądzie zmieni się tak: Jak widać w kolumnie Value zamiast {TestApp.Form1.TestClass1} pojawiła się informacja o ilości elementów. Jest to typowe zastosowanie tego atrybutu. Kolejny przykład: [System.Diagnostics.DebuggerDisplay("Count = {Collection.Length}")] [System.Diagnostics.DebuggerDisplay("First = {Collection[0]}")] Jak widać choć kod się kompiluje, to efekt jest raczej błędny, pierwszy atrybut został zignorowany. W pierwszym przykładzie, kiedy klasa nie była opatrzona atrybutem, w kolumnie Value wyświetliło się {TestApp.Form1.TestClass1}. Informacja ta tak naprawdę podchodzi z funkcji ToString(). Nadpisując tę funkcję możemy zmienić zawartość kolumny Value bez używania atrybutów. Oczywiście nikt nie broni skorzystać nam z jednego i drugiego. Kolejny przykład:
[System.Diagnostics.
DebuggerDisplay("{ToString()}")]
class TestClass1
{
   [System.Diagnostics.DebuggerBrowsable( System.Diagnostics. DebuggerBrowsableState.RootHidden)]
   public int[] Collection
   {
       get
       {  
           return new int[] { 1, 2, 3, 4 };
       }
   }
}

class TestClass2
{
   [System.Diagnostics.DebuggerBrowsable( System.Diagnostics. DebuggerBrowsableState.RootHidden)]
   public int[] Collection
   {
       get
       {
           return new int[] { 1, 2, 3, 4 };
       }
   }
}
O co chodzi z tym plusem ? Specyfikuje on nam klase wewnętrzną. Jeśli dla klasy TestClass2 zdefiniujemy metodę:
public override string ToString()
{
   return base.ToString();
}
, to w obu stringach będzie plus. Nie wiem czemu ten plus tak się zachowuje. Enum Rozpatrzmy taki przykład:
[System.Diagnostics.DebuggerDisplay("{this ==  TestEnum.Zatrzymany ?  \"Off\" : \"On\"}")]
enum TestEnum
{
   Zatrzymany,
   Uruchomiony
}

class TestClass1
{
   public static TestEnum enum1 = TestEnum.Uruchomiony;
   public static TestEnum enum2 = TestEnum.Zatrzymany;
}
Jak widać w klamerkach {} może się znajdować dowolny kod. Szczerze dla enum nie widzę sensownego zastosowania za wyjątkiem tłumaczenia nazw enumeracji na bardziej czytelne. Zobaczmy jak wygląda definicja enum w kodzie pośrednim:
[DebuggerDisplay("{this ==  TestEnum.Zatrzymany ?  \"Off\" : \"On\"}")]
private enum TestEnum
{
   Zatrzymany,
   Uruchomiony
}
Widzimy, że łańcuch znaków podany w klamerkach {} nie jest w żaden sposób kompilowany. Jak więc zachowa się debuger jak będziemy się starali debugować naszą klasę z poziomu np. VB. Generalnie jeśli VB, albo inny język nie rozpozna składni dostaniemy błąd. Najpewniejszym sposobem jest stworzenie specjalnej metody i wywoływanie jej z poziomu atrybutu. Takie rozwiązanie powinno zapewnić wysoką przenoszalność kodu. A najlepiej ograniczyć się tylko do napisania metody ToString(). Przy okazji warto jeszcze wspomnieć, że możemy wpływać także na kolumnę Type. Taki atrybut dla enum: [System.Diagnostics.DebuggerDisplay("{this == TestEnum.Zatrzymany ? \"Off\" : \"On\"}", Type = "Nie wiem")] Spowoduje zmianę w kolumnie Type dla obu zmiennych klasy TestClass1. Oczywiście typ można podmienić także dla innych elementów dla których możemy zaaplikować atrybut DebuggerDisplayAttribute. Ja osobiście nie widzę powodu dla którego mielibyśmy modyfikować wartość tej kolumny. Delegate Rozpatrzmy taki przykład:
class TestClass1
{
   [System.Diagnostics.DebuggerDisplay("{ToString()}")]
   public delegate void SimpleDelegateA();

   public delegate void SimpleDelegateB();

   public SimpleDelegateA simpleDelegateA1;
   public SimpleDelegateA simpleDelegateA2;

   public SimpleDelegateB simpleDelegateB1;
   public SimpleDelegateB simpleDelegateB2;

   public TestClass1()
   {
       simpleDelegateA1 = new SimpleDelegateA(TestFunc);
       simpleDelegateB1 = new SimpleDelegateB(TestFunc);
   }

   public void TestFunc()
   {
   }
}
Z analizy tego przykładu możemy wyciągnąć wniosek, że na wszystkie delegaty został zaaplikowany atrybut w postaci {Method = {Method}}. Powiem o tym w dalszej części postu. Przy okazji powiedzmy sobie pewną rzecz o klamerkach {}. Chcemy pokazać w debugerze takie coś: {Method = {Void TestFunc()}}. Tak powinna wyglądać definicja atrybutu: [System.Diagnostics.DebuggerDisplay(@"\{Methodx = {Method}\}")] [System.Diagnostics.DebuggerDisplay("\\{Methody = {Method}\\}")] [System.Diagnostics.DebuggerDisplay(@"\{Methodx = {Method}}")] [System.Diagnostics.DebuggerDisplay("\\{Methody = {Method}}")] Wszystkie 4 podejścia są poprawne. Ogólnie debuger bierze informację w klamerkach {} i stara się ją skompilować. Poprzedzając klamerkę \ mówimy debugerowi, że tak klamerka, to nie jest taka o, którą mu chodzi, ona należy do tekstowej części informacji. Klamerki kończącej nie musimy tutaj unieważniać gdyż pierwsza klamerka zamykająca, po otwierającej, jest tą o którą nam chodzi. Tutaj podaje mało sensowny przykład na potrzebę unieważnienia klamerki zamykającej: [System.Diagnostics.DebuggerDisplay(@"\{Method = {Method + ""_\}_"" }}")] Pola, Właściwości Przykład:
class TestClass1
{
   [System.Diagnostics.DebuggerDisplay("{Value}", Name = "{Key}")]
   public class Pair
   {
       public int Key;
       public string Value;
   }

   [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.RootHidden)]
   public Pair[] Collection
   {
       get
       {
           return new Pair[] { new Pair() { Key = 2, Value = "two" }, new Pair() { Key = 4, Value = "four" } };
       }
   }
}
Tutaj mamy przykład połączenia dwóch atrybutów DebuggerDisplayAttribute i DebuggerBrowsableAttribute. Assembly Informacja tutaj podana odnosi się do VS2008. Wbrew pozorom nie nakładamy tego atrybutu na dowolny zestaw. Debuger nie uwzględni tej informacji. Przynajmniej tak wynikło z moich eksperymentów. W katalogu Moje Dokumenty idziemy do Visual Studio 2008\Visualizers. Interesują nas tam dwa pliki: autoexp.cs i autoexp.dll (polecam zrobić sobie ich kopię). Biblioteka to skompilowany plik źródłowy. Tworzymy nowy projekt biblioteki klas, usuwamy z projektu wszystkie pliki *.cs. Ddoajemy do projektu plik autoexp.cs. Zmieniamy nazwę zestawu w opcjach projektu na autoexp. Teraz możemy przystąpić do modyfikacji źródeł. Jako przykład dodajemy: [assembly: DebuggerDisplay(@"\{Count = {Collection.Length}}", Target = typeof(TestApp.TestClass1))] Bardzo ważna uwaga. Jeśli naszym celem będzie klasa wewnętrzna nasze zmiany nie zadziałają. Kompilujemy i dodajemy utworzoną bibliotek do moich dokumentów. Restartujemy VS, i teraz jeśli użyjemy klasy TestClass1 to debuger powinien uwzględnić nasze zmiany. Możemy także bez problemu zmodyfikować zawartość tego pliku jeśli coś w oryginalnej wizualizacji nam się nie podoba. Tutaj właśnie jest dodawany wpis, który modyfikuje sposób wyświetlania informacji o delegatach: [assembly: DebuggerDisplay(@"\{Method = {Method}}", Target = typeof(System.Delegate))] Informacje uzupełniające Co jeśli podczas ewaluacji zmiennej do podglądu zdarzy się wyjątek ? Jak widać zostaniemy o tym poinformowani, nie przerwie to w żaden sposób obliczania zawartości pozostałych podglądanych elementów. Jakim poziomem widzialności powinny się charakteryzować elementy z klamerkach {} ?
public class TestClass2
{
   private static int XX = 4;
}

[System.Diagnostics.DebuggerDisplay("{TestClass2.XX}")]
public class TestClass1
{
   [System.Diagnostics.DebuggerDisplay("{System.DateTime.Now.dateData}")]
   public int[] Collection
   {
       get
       {
           return new int[] { 1, 2, 3, 4 };
       }
   }

   private int GetPrivate()
   {
       return 5555;
   }
}
Taki kod będzie działał. Czyli odwołanie do prywatnych metod zarówno klasy w tej samej przestrzeni nazw, jak i w innej przestrzeni nazw (pola prywatnego klasy DateTime będzie działać. Z jednej strony można to nazwać błędem. Z drugiej Watch powinien mieć dostęp do prywatnych składników klasy. Czy w klamerkach mogą się znajdować wyrażenia oddzielone średnikami ? Nie, To co jest w klamerkach brane jest jako wyrażenie, które musi coś zwracać. Na potwierdzenie tego, jeśli wyrażenie w klamerkach nie zawiera średników i nic nie zwraca, otrzymujemy w Watch-u taki błąd: Expression has been evaluated and has no value W przypadku, gdy wyrażenie zawiera średniki, debuger zachowuje się tak jakby atrybutu wogóle nie było. Do czego tak naprawdę możemy się odwołać z wyrażenia w klamerkach {} ? Metody statyczne klas i metody obiektów, właściwości statyczne klas, właściwości obiektów, pola statyczne klas, pola obiektów, delegaty, zdarzenia. W samych klamerkach może być dowolny kod, który kompilator C# może skompilować. Prawie... Nie można używać wyrażeń lambda.
[System.Diagnostics.DebuggerDisplay("{Multiple(d => 2)}")]
public class TestClass1
{
   public int Multiple(Func f)
   {
       return f(2) * 2;
   }
}
Dostaniemy błąd: Expression cannot contain lambda expressions Modyfikator nq Rozpatrzmy taki przykład:
[System.Diagnostics.DebuggerDisplay("Name: {Name}, Age: {Age}")]
public class TestClass1
{
   public String Name
   {
       get
       {
           return "name";
       }
   }

   public int Age
   {
       get
       {
       return 56;
       }
   }
}

[System.Diagnostics.DebuggerDisplay("Name: {Name, nq}, Age: {Age}")]
public class TestClass2
{
   public String Name
   {
       get
       {
           return "name";
       }
   }

   public int Age
   {
       get
       {
           return 56;
       }
   }
}
Debugger pokaże: Jak widać za pomocą nq możemy poinstruować debuger by wyrażenia, które są stringami, nie zamykać w cudzysłowach. Problem z widocznością klas używanych w klamerkach {} Weźmy taki przykład:
[System.Diagnostics.DebuggerDisplay("{System.Int32.Parse(\"5\")}")]
public class TestClass1
{
}

[System.Diagnostics.DebuggerDisplay("{Int32.Parse(\"5\")}")]
public class TestClass2
{
}
Debuger pokaże nam: Widzimy, że używając dowolnej klasy spoza naszej przestrzeni nazw trzeba zawsze podać pełną nazwę klasy. Czy atrybut podlega dziedziczeniu ? Weźmy taki przykład:
[System.Diagnostics.DebuggerDisplay("{System.Int32.Parse(\"7\")}")]
public class TestClass1
{
   [System.Diagnostics.DebuggerDisplay("{System.Int32.Parse(\"8\")}")]
   public virtual int X
   {
       get
       {
           return 5;
       }
   }
}

[System.Diagnostics.DebuggerDisplay("{System.Int32.Parse(\"9\")}")]
public class TestClass2: TestClass1
{
   public override int X
   {
       get
       {
           return 5;
       }
   }
}
Takie coś zobaczymy podczas debugowania: Gdyby klasa TestClass2 była pozbawiona atrybutu to został by uwzględniony atrybut z klasy bazowej, tak jak to się dzieje z właściwością (to zachowanie zostało ustawione w atrybutach atrybutu DebuggerDisplayAttribute). Widzimy, że możemy zobaczyć co generuje atrybut właściwości dla klasy TestClass1 w widoku klasy bazowej, tego samego nie możemy powiedzieć o samej klasie. Poza tym warto zauważyć, jak można zafałszować to co pokazuje debuger na przykładzie właściwości X, gdzie możemy sprawić, że w kolumnie Value może zostać pokazane zupełnie co innego.

System.Diagnostic.DebuggerBrowsableAttribute

Za pomocą tego atrybutu kontrolujemy jak środowisko debugujące (Visual Studio) wyświetla nam informacje o stanie zmiennych (pól i właściwości) podczas zatrzymanego wykonywania programu. Reguły definiowane przez ten atrybut dotyczą zarówno okienek wyskakujących po najechaniu na zmienną, jak i tego co pojawia się w Watch, Autos, Locals.
public class TestClass1
{
    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.Collapsed)]
    public DateTime dt1 = DateTime.Now;

    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.Never)]
    public DateTime dt2 = DateTime.Now;

    public DateTime dt3 = DateTime.Now;
}


public class TestClass2
{
    private int y;

    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.Never)]
    private int d;

    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.Never)]
    private int e;

    public int D
    {
        get
        {
            return d;
        }
        set
        {
            d = value;
        }
    }

    public int E
    {
        get
        {
            y = System.DateTime.Now.Millisecond;
            return e;
        }
        set
        {
            e = value;
        }
    }

    public int X
    {
        get;
        set;
    }
}

public class TestClass3
{
    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.RootHidden)]
    public DateTime dt1 = DateTime.Now;

    public int X;
}

public class TestClass4
{
    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.RootHidden)]
    public DateTime dt1 = DateTime.Now;


    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.RootHidden)]
    public DateTime dt2 = DateTime.Now;

    public int X;
}

public class TestClass5 : IList<int>
{
    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.RootHidden)]
    private int[] Collection
    {
        get
        {
            return new int[] { 1, 2, 3, 4 };
        }
    }

    public int IndexOf(int item)
    {
        throw new NotImplementedException();
    }

    public void Insert(int index, int item)
    {
        throw new NotImplementedException();
    }

    public void RemoveAt(int index)
    {
        throw new NotImplementedException();
    }

    public int this[int index]
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public void Add(int item)
    {
        throw new NotImplementedException();
    }

    public void Clear()
    {
        throw new NotImplementedException();
    }

    public bool Contains(int item)
    {
        throw new NotImplementedException();
    }

    public void CopyTo(int[] array, int arrayIndex)
    {
        throw new NotImplementedException();
    }

    public int Count
    {
        get 
        {
            return Collection.Length;
        }
    }

    public bool IsReadOnly
    {
        get 
        {
            return true;
        }
    }

    public bool Remove(int item)
    {
        throw new NotImplementedException();
    }

    public IEnumerator<int> GetEnumerator()
    {
        return Collection.Cast<int>().GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public partial class Form1 : Form
{
    public void Test()
    {
        TestClass1 tc1 = new TestClass1();
        TestClass2 tc2 = new TestClass2();
        TestClass3 tc3 = new TestClass3();
        TestClass4 tc4 = new TestClass4();
        TestClass5 tc5 = new TestClass5();

        tc1.ToString();
        tc2.ToString();
        tc3.ToString();
        tc4.ToString();
        tc5.ToString();
    }

    public Form1()
    {
        InitializeComponent();

        Test();
    }
}
Zobaczmy jak debuger wyświetla informacje o poszczególnych zmiennych (breakpoint jest ustawiony w metodzie Test() po stworzeniu zmiennych lokalnych. Poniżej zrzuty ekranu z zawartości Watch-a:
Konstruktor atrybutu System.Diagnostics.DebuggerBrowsable przyjmuje jako parametr wartość enumeracji System.Diagnostics.DebuggerBrowsableState. W kodzie obiektu TestClass1 pokazano zastosowanie dwóch z nich. Pole dt1 zostało oznaczone jako Collapsed. Jest to domyślne zachowanie debugera i tak naprawdę sposób pokazywania zmiennej dt1 będzie taki sam, jak sposób pokazywania zmiennej dt3. Pole dt2 zostało oznaczone jako Hidden i tym samym debuger go nie pokazuje. Ale jak możemy zauważyć na pierwszym zrzucie nic nie stoi na przeszkodzie by wymusić pokazywanie pola ukrytego. W TestClass2 widzimy typowe zastosowanie Hidden, kiedy chcemy ukryć dublujące się wartości pół i odpowiadających im właściwości, by pokazywana informacja była bardziej czytelna. Ogólnie powinniśmy ukryć to co jest nieistotne z punkty widzenia programisty korzystającego z obiektu. Nie ma to takiego znaczenia jeśli piszemy dla siebie, ale jeśli tworzymy jakiś komponent, który może być używany przez innych, to warto sprawić, by nie musieli się oni przejmować debugując, nieistotnymi polami. Jest tutaj pewna analogia do deklarowania widoczności jako private i protected. W przykładzie tym możemy zauważyć także pewną pułapkę. Za każdym razem gdy najedziemy na zmienną tc2 to zobaczymy inną wartość zmiennej y. Debuger wyświetlając właściwości wywołuje ich metody get, jeśli kod tam zawarty w jakikolwiek sposób zmienia obiekt, to możemy mieć problem. Warto o tym pamiętać. Jeśli podczas ewaluacji właściwości zostanie wyrzucony wyjątek to zostaniemy o tym poinformowani: Pozostała nam do omówienia ostatnia wartość enumeracji System.Diagnostics.DebuggerBrowsableState RootHidden. Na przykładzie TestClass3 i TestClass4 widzimy, że wszystkie właściwości i pola klasy DateTime stają się właściwościami i polami klasy zawierającej. Jednak przykłady te nie ilustrują korzyści jakie atrybut tego typu nam daje. Przykład z TestClass4 jest już ekstremalnie głupi. Sens RootHidden widać w TestClass5 kiedy oznaczona tym atrybutem zmienna implementuje interfejs IEnumerable. Kolekcja znajdująca się pod właściwością Collection staje się jakby elementem klasy zawierającej. Jest to szczególnie przydatne kiedy implementujemy klasę, która jest pojemnikiem na inne obiekty, przy czym wcale nie musimy tutaj implementować interfejsu jakiejś kolekcji. I ostatni przykład TestClass6:
public class TestClass6
{
    [System.Diagnostics.DebuggerBrowsable (System.Diagnostics. DebuggerBrowsableState.RootHidden)]
    private List<int> Collection
    {
        get
        {
            return new List<int> { 1, 2, 3, 4 };
        }
    }
}
Widok w Watch-u: Widzimy, że pojawił się element drzewa Raw View, w którym dodatkowo możemy się zapoznać z ukrytymi elementami List<int>.

2009-06-01

System.Diagnostic.ConditionalAttribute

Atrybut System.Diagnostics.ConditionalAttribute można stosować do metod i klas atrybutów. Jeśli podany w System.Diagnostic.ConditionalAttribute symbol kompilacji warunkowej istnieje metoda będzie przetwarzana, zaś atrybut wyemitowany. Za pomocą tego atrybutu realizujemy kompilację warunkową. W poniższym przykładzie rozpatrzymy tylko stosowanie atrybutów kompilacji warunkowej dla metod. Jeśli idzie o atrybuty to bardzo wiele metod z przestrzeni nazw System.Diagnosic ma na metody nałożony atrybut: [Conditional("DEBUG")], który gwarantuje wywoływanie metod tylko w konfiguracji debugowania.
#define TEST1
//#define TEST2
#define TEST3
//#define TEST4

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Linq;

namespace TestApp
{
   public partial class Form1 : Form
   {
       [System.Diagnostics.Conditional("TEST1")]
       public void Test1()
       {
       }

       public int GetXX()
       {
           return System.DateTime.Now.Millisecond;
       }

       [System.Diagnostics.Conditional("TEST2")]
       public void Test2(int xx)
       {
       }

       #if TEST3
       public void Test3()
       {
       }
       #endif

       #if TEST4 && TEST3
       public void Test4()
       {
       }
       #endif

       public Form1()
       {
           InitializeComponent();

           Test1();

           if (GetXX() == 56)
               Test2(GetXX());

           Test3();

           System.Diagnostics.Debug.Close();

           #if TEST4
           Test4();
           #endif
       }
   }
}
Wyemitowany kod pośredni:
public class Form1 : Form
{
   // Fields
   private IContainer components;

   // Methods
   public Form1()
   {
       this.InitializeComponent();
       this.Test1();
       this.GetXX();
       this.Test3();
   }

   protected override void Dispose(bool disposing)
   {
       if (disposing && (this.components != null))
       {
           this.components.Dispose();
       }
       base.Dispose(disposing);
   }

   public int GetXX()
   {
       return DateTime.Now.Millisecond;
   }

   private void InitializeComponent()
   {
       base.SuspendLayout();
       base.AutoScaleDimensions = new SizeF(6f, 13f);
       base.AutoScaleMode = AutoScaleMode.Font;
       base.ClientSize = new Size(0x1d9, 0x127);
       base.Name = "Form1";
       this.Text = "Form1";
       base.ResumeLayout(false);
   }

   [Conditional("TEST1")]
   public void Test1()
   {
   }

   [Conditional("TEST2")]
   public void Test2(int xx)
   {
   }

   public void Test3()
   {
   }
}
W przykładzie tym możemy zaobserwować zachowanie kompilacji warunkowej zrealizowanej przy pomocy atrybutów i przy pomocy komend preprocesora. Zgodnie z oczekiwaniem do kodu pośredniego emitowane są tylko wywołania metod Test1() i Test3(). Choć metoda Test2() nie jest wywoływana to jej ciało jest emitowane. W przypadku komend preprocesora cały kod w #if ... #endif jest przez preprocesor wycinany przed przystąpieniem do kompilacji. Na podstawie atrybutu warunkowego nie możemy zdecydować o tym czy danej metody nie emitować. W innym pliku bowiem może znaleźć się definicja #define TEST2, co spowoduje, że dla tamtejszych metod Test2() zostanie wyemitowane ich wywołanie. Atrybut warunkowy można nałożyć tylko na metody, które zwracają nie zwracają wyniku. Gdyby nie ograniczenie kompilator miałby np. problem z kompilacja wyrażenia matematycznego, którego jeden element jest metodą warunkową. No i jeszcze jedna sprawa. Porównajmy sobie dwie takie definicje:
[Serializable]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
[ComVisible(true)]
public sealed class ConditionalAttribute : Attribute

[Serializable]
[ComVisible(true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public sealed class DebuggerStepThroughAttribute : Attribute
Trochę się różnią, ale pytanie brzmi gdzie tu jest ograniczenie nie pozwalające nałożyć atrybut warunkowy na metodę, która coś zwraca. I po drugie gdzie tu jest powiedziane, że atrybut warunkowy nie możemy nałożyć na klasę, tylko na inne atrybuty warunkowe ? Które podejście jest lepsze. Wadą atrybutu warunkowego jest to, że kod metody pomimo tego, że nie jest używana, zostanie wyemitowany. Jeśli naszą intencją jest dostarczenie okrojonej wersji programu i dodatkowo chcemy ukryć działanie pewnych metod to to podejście nam nie zapewni. Jeśli wywołanie metody ma zależeć od kombinacji logicznej kilku atrybutów to tylko komendy preprocesora dają nam taką możliwości (za pomocą atrybutów warunkowych możemy co najwyżej zrealizować warunek logicznego i). Jeśli chcemy dostarczyć innego zachowania metody dla zdefiniowanego symbolu, a innego dla jego braku (typowe DEBUG i RELEASE) to atrybut warunkowy nam tego nieumożliwi. Za pomocą atrybutu warunkowego nie możemy nałożyć warunku tylko na część metody. Wadą komend preprocesora jest to, że muszą być ona nakładane zarówno na definicję metody jak i jej wywołania. Jeśli metoda jest wywoływana setki razy, każde jej wywołanie musi być ujęte w stosowny blok preprocesora, a każda modyfikacja warunku #if wymaga bardzo wielu zmian. Kompilacja warunkowa zrealizowana za pomocą komend preprocesora nie nadaje się najlepiej do wywołania warunkowej metody z jednego modułu w innym module, kiedy moduł ten nie mamy w postaci źródeł, ale w postaci skompilowanej. Po skompilowaniu taka metoda albo jest albo jej nie ma. W przypadku kompilacji warunkowej jest zawsze, a dodatkowo zachowany jest atrybut kompilacji warunkowej. Dzięki czemu definiując symbol w tym atrybucie sprawiamy, że metoda jest wywoływana. Innymi słowy atrybut warunkowy pozostawia po sobie pewną infrastrukturę (informację) o możliwości warunkowego wykorzystania kodu w danym module. Powiedzmy sobie od razu, że C# to nie jest C++, gdzie skomplikowane #if zdarzają się bardzo często zwłaszcza jeśli chodzi o projekty wieloplatformowe. Java wogóle nie ma żadnych metod kompilacji warunkowej, tam takie sprawy załatwia się zdefiniowaniem zmiennej finalnej, a metody warunkowe wywołuje się poprzez if na tej zmiennej. Kompilator, jeśli warunek jest fałszywy nie emituje wtedy całego bloku if. W przypadku Javy jeśli mamy intensywne logowanie połączone z skomplikowanym formatowaniem stringów, sam if we wnętrzu metody logującej nie wystarczy. Co prawdą logowanie nie nastąpi ale możemy mnóstwo czasu stracić na formatowaniu stringu. Tej wady pozbawiony jest atrybut warunkowy w C#. Metoda nie tylko zostanie nie wywołana, ale także jej parametry nie będą przetwarzane. Widzimy to na przykładzie metody Test2(). Dodatkowo warto zwrócić uwagę, że warunek logiczny zostanie wyemitowany. Wspomniana metoda kompilacji warunkowej zastosowana w Javie także zadziała w C#. Zauważmy, że kompilacja warunkowa jest domeną kompilatora. W kodzie pośredniego nie ma po niej śladu. Jedyne co pozostaje to atrybuty warunkowe, dzięki czemu inne metody mogą skorzystać z warunkowego wywołania tamtych metod. Przy czym znowu atrybuty te są używane przez kompilator emitujący kod pośredni. Osobiście uważam, że dopóki naprawdę nie jest nam potrzebne #if ... #endif powinniśmy korzystać z atrybutów warunkowych. Warto też zauważyć chyba oczywistą rzecz, że w bloku #if ... #endif jeśli warunek jest nie spełniony może się znajdować dowolny tekst. Metoda oznaczona atrybutem warunkowym, nawet jeśli nie zostanie wywołana, ciągle musi mieć poprawnie zdefiniowane parametry. Innymi słowy wywołanie tej metody musi się kompilować.

2009-05-31

Dziwny błąd składni

Krótko chodzi o to, że zarówno VS2008, jak i VS2010 nie przyjmują takiej konstrukcji składniowej:
new [] {1, 2, 3}.ForEach(
    n => throw new Exception() );
Taka składnia jest poprawna:
new [] {1, 2, 3}.ForEach(
    n => new Exception() );
new[] { 1, 2, 3 }.ForEach(
    n => { throw new Exception() } );

Szybkośc działania .NET 4.0 Beta

Ciężko jest przeprowadzić dokładne testy. Sprawdzone zostaną dwa moje programy: jeden instesywnie liczący program do renderingu, drugi intensywnie wykorzystujący LINQ to objects solver do sudoku i metoda Math.Round(). Math.Round() jest jakieś dwa razy szybsze.
public void Test()
{
   System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
   sw.Start();
   for (int i = 0; i < 10000; i++)
   {
       for (int j = 0; j < 1000; j++)
       {
           int xx = (int)Math.Round(i * j / 39.4);
       }
   }
   sw.Stop();
   System.Console.WriteLine("Math.Round(): {0}", sw.ElapsedMilliseconds);
}
To była pierwsza rzecz jaką sprawdziłem. Zapamiętałem ją, bo profilując jedną aplikację okazało się, że idzie na to stosunkowo dużo czasu. A jak się przyjrzałem natywnym debugerem (WinDbg) co się tam dzieje w tej metodzie, to wiedziałem jedno, że jak zależy nam na prędkości to lepiej napisać swoją własną wersję. Jak widać było co tam przyspieszać. Drugi test dotyczył szybkości działania LINQ to objects. Niestety źródeł aplikacji tutaj nie mogę podać. Jest to aplikacja rozwiązująca Sudoku przy pomocy właśnie LINQ. Można powiedzieć, że LINQ jest tak samo szybkie (a raczej wolne) jak było. No i test trzeci, moja własna aplikacja do raytracingu. Tutaj także nie ma się co rozpisywać, prędkość renderowania przykładowej sceny nie zmieniła się. Czyli można powiedzieć, że za wyjątkiem pojedyńczych funkcji, które ewidentnie były wolne, ogólnego przyspieszenia nie należy się spodziewać. Cały proces konwersji solucji i projektów do VS2010, jak i kompilacji przebiegał bezproblemowo.