2009-12-23

Problem z skalowaniem i przywracaniem okien pod Linuxem

Problem ujawnia się po zainstalowaniu sterowników własnościowych od ATI. Przywrócenie zminimalizowanego okna lub zmiana jego rozmiaru trwa 2-3 sekundy. Takie samo opóźnienie towarzyszy tworzeniu nowego okna. Miałem go w Ubuntu 9.04, ale jako, że nie specjalnie często korzystałem z tego systemu to go zignorowałem. W sieci też żadnego rozwiązania tego problemu nie mogłem znaleźć. Z tego co poczytałem problem ten dotyczył prawie wszystkich kart ATI. Teraz dokonałem aktualizacji do Ubuntu 9.10. Minęło pół roku i co ? I dalej to samo. Po prosty nie da się pracować. Można oczywiście wyłączyć sterowniki od ATI, ale wtedy nie miałem akceleracji i wszystko działało wolno. I tak źle i tak niedobrze. Tutaj mamy dosyć dobre omówienie problemu. Data pierwszego postu to styczeń 2009. Czyli to prawie rok odkąd błąd się pojawił. Wbrew pozorom problem nie leżał w sterownikach od ATI. W tamtej dyskusji pojawiało się wiele postów z tym związanych. Okazało się, że winowajcą jest XServer. Mieli oni jakieś problemy z grafikami od Intela polegający na tym, że na ekranie pojawiały się śmieci, więc dopisali kod, który coś tam czyści. Problem w tym, że w układy graficzne Intela współdzielą pamięć z procesorem, zaś większość kart ATI nie. I wydajność zabijało przesyłanie danych pomiędzy pamięcią karty, a procesora. Tutaj znajdziemy dwa rozwiązania tego problemu. Pierwsze rozwiązanie Dodajemy do źródeł oprogramowania: deb http://ppa.launchpad.net/ubuntu-x-swat/xserver-no-backfill/ubuntu karmic main Można też do zrobić z linii poleceń: gksu gedit /etc/apt/sources.list Wykonujemy odświeżenie dostępnych aktualizacji i teraz w Menadżerze aktualizacji powinny pojawić się odpowiednio zmodyfikowany XSerwer. Drugie rozwiązanie Pobieramy 107_fedora_dont_backfill_bg_none.patch. I zapomocą tych poleceń przygotujemy XServer do zbudowania, ściągamy jego źródła, nakładamy patcha, bodujemy do pakietu deb i go instalujemy. sudo apt-get build-dep xorg-server apt-get source xorg-server cd xorg-server-1.6.4 patch -p1 < ./../107_fedora_dont_backfill_bg_none.patch debuild cd .. sudo dpkg --install xserver-xorg-core*.deb Zwróćcie uwagę na poprawność ścieżek.

2009-11-15

LINQ - Contains nie operuje na zbiorach

Podobnie jak Except zachowuje się Contains. Oto alternatywna metoda operująca na zbiorach:
public static bool ContainsExact<T>(this IEnumerable<T> a_enumerable, IEnumerable<T> a_values)
{
    List<T> list = new List<T>(a_enumerable);

    if (a_values.FirstOrDefault() == null)
        return true;

    foreach (T ele in a_values)
    {
        int index = list.IndexOf(ele);
        if (index == -1)
            return false;
        else
            list.RemoveAt(index);
    }

    return true;
}

LINQ - równość dwóch zbiorów

Poniższa metoda rozszerzona sprawdza czy oba zbiory zawierają takie same elementy:
public static bool Exact<T>(this IEnumerable<T> a_enumerable, IEnumerable<T> a_values)
{
    List<T> list = new List<T>(a_values);

    int init_count = list.Count;
    int count = 0;

    foreach (T ele in a_enumerable)
    {
        count++;

        if (count > init_count)
            return false;

        int index = list.IndexOf(ele);
        if (index == -1)
            return false;
        else
            list.RemoveAt(index);
    }

    return count == init_count;
}

2009-11-09

LINQ - Except nie operuje na zbiorach

Przykład:
int[] ar1 = { 1, 2, 3, 3 };
int[] ar2 = { 1, 3 };
int[] ar3 = ar1.Except(ar2).ToArray();
int[] ar4 = ar1.Intersect(ar2).ToArray();
Tablica ar3 będzie zawierać jeden element 2. Przydałaby się metoda, która zachowywałaby się jak odjęcie zbioru od zbioru:
public static IEnumerable<T> Substract<T>(this IEnumerable<T> a_enumerable, IEnumerable<T> a_values)
{
    List<T> list = new List<T>(a_values);

    foreach (T ele in a_enumerable)
    {
        int index = list.IndexOf(ele);
        if (index != -1)
            list.RemoveAt(index);
        else
            yield return ele;
    }
}
Teraz:
int[] ar5 = ar1.Substract(ar2).ToArray();
zwróci nam zbiór { 2, 3}.

2009-10-28

Zła jakość ikon w ToolStrip

Jeśli w toolbarze mamy ustawiony rozmiar wyświetlania obrazków (ImageScalingSize) inny od natywnego rozmiaru obrazków, to zmuszamy system by go za nas przeskalował. Problem w tym, że radzi on sobie z tym wyjątkowo nieudolnie (obrazki wychodzą kiepskiej jakości). Nie udało mi się w żaden sposób wpłynąć na ten proces, tak więc nie pozostało nic innego zrobienie ręcznego skalowania podczas uruchamiania aplikacji. Poniższy kod trzeba będzie rozbudować jeśli w naszej aplikacji podczas jej działania dopuszczamy zmianę wielkości ikon toolbara. Samo przeskalowanie jest stosunkowe prosta:
for (int i = 0; i < toolStrip.Items.Count; i++)
{
    if (toolStrip.Items[i].Image != null)
    {
        toolStrip.Items[i].Image = toolStrip.Items[i].Image.GetThumbnailImage(
            toolStrip.ImageScalingSize.Width, toolStrip.ImageScalingSize.Height, null, IntPtr.Zero);
    }
}
Jeśli to rzeczywiście błąd, a nie, że tylko ja nie wiem, gdzie ustawić jaką właściwość, to jest to dość fatalny błąd. Inna sprawa czemu kolekcja Items nie bazuje na typach generycznych, żeby foreach nie zwracało obiektów typu Object. Ten problem jest zresztą bardziej powszechny. Np. Enum.GetValues zwraca obiekt typu Array. Może to kiedyś uporządkują, choć będzie się to wiązało z utratą kompatybilności wstecz.

2009-10-21

Kasowanie całego katalogu

Prosta funkcja kasująca cały katalog wraz z plikami i podkatalogami. Standardowa procedura kasowania katalogu wymaga by całe poddrzewo katalogów nie zawierało plików. Jak zawsze przy korzystaniu z takich funkcji trzeba uważać bo można sobie zrobić kuku. Funkcja ta nie stara się w żaden sposób przejąć prawa do tego do czego nie ma prawa (tzn. zmiana uprawnień plików, zmiana atrybutu read-only). Jak coś się nie uda to będzie wyjątek. A oto kasująca wszystko metoda rozszerzona:
public static class DirectoryInfoExtensions
{
    public static void DeleteAll(this DirectoryInfo a_dir_info)
    {
        if (!a_dir_info.Exists)
            return;

        foreach (FileInfo file_info in a_dir_info.GetFiles())
            file_info.Delete();

        foreach (DirectoryInfo dir_info in a_dir_info.GetDirectories())
            dir_info.DeleteAll();

        a_dir_info.Delete(false);
    }
}

2009-10-11

LINQ - rozszerzenie Except

Oto proste uzupełnienie metody Except(). Bardzo często jej szukam w liście podpowiadającej tak więc teraz jest:
public static IEnumerable<T> Except<T>(this IEnumerable<T> a_enumerable, T a_element)
{
    foreach (T ele in a_enumerable)
    {
        if (!ele.Equals(a_element))
            yield return ele;
    }
}

Uproszczona inicjalizacja kolekcji

Przykład:
public void Test()
{
    List ar1 = new List { new Point(3, 4), new Point(5, 6) };
    ar1.ToString();
}
Kod IL:
.method public hidebysig instance void Test() cil managed
{
    .maxstack 4
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point> ar1,
        [1] class [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point> <>g__initLocal2d5)
    L_0000: newobj instance void [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point>::.ctor()
    L_0005: stloc.1 
    L_0006: ldloc.1 
    L_0007: ldc.i4.3 
    L_0008: ldc.i4.4 
    L_0009: newobj instance void [System.Drawing]System.Drawing.Point::.ctor(int32, int32)
    L_000e: callvirt instance void [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point>::Add(!0)
    L_0013: ldloc.1 
    L_0014: ldc.i4.5 
    L_0015: ldc.i4.6 
    L_0016: newobj instance void [System.Drawing]System.Drawing.Point::.ctor(int32, int32)
    L_001b: callvirt instance void [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point>::Add(!0)
    L_0020: ldloc.1 
    L_0021: stloc.0 
    L_0022: ldloc.0 
    L_0023: callvirt instance string [mscorlib]System.Object::ToString()
    L_0028: pop 
    L_0029: ret 
}
Widzimy, że inicjalizacja odbywa się poprzez wywołanie metody Add obiektu. Cała konstrukcja listy odbywa się poprzez zmienną tymczasową, dzięki czemu proces ten jest atomowy. Zauważmy też że podczas konstrukcji obiektu nie musimy pisać new List(). Jakie warunki musi spełniać klasa, by użycie takiej inicjalizacji było możliwe? Warunki są dwa. Po pierwsze klasa musi implementować interfejs System.Collections.IEnumerable. Po drugie musi zawierać metodę Add(). Czemu samo posiadanie przez klase metody Add() nie wystarczy nie wiem. Skoro już dano takie ograniczenie o wiele rozsądniejsze było by wymaganie implementacji interfejsu ICollection, który wymaga implementacji metody Add(). Przykład własnej klasy, która pozwala nam na skróconą inicjalizację kolekcji:
class TestA : IEnumerable
{
    public void Add(int a)
    {
    }

    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

public void Test()
{
    TestA testa = new TestA { 1, 2 };
    testa.ToString();
}
Wariacje na temat typu parametru metody Add() sobie odpuścimy. Wygląda na to, że musi istnieć bezpośrednia konwersja pomiędzy parametrem metody, a składnikiem inicjalizacji. Teraz przyjrzyjmy się bliżej metodzie Add(). Oto przykład dla metody o dwóch parametrach:
class TestB : IEnumerable
{
    public void Add(int a, int b)
    {
    }

    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

public void Test()
{
    TestB testb = new TestB { {1, 2}, {2, 3} };
    testb.ToString();
}
Jest o tyle ważne, że podobnie jak listę możemy inicjalizować słownik:
public void Test()
{
    Dictionary dict = new Dictionary() { { Color.Red, "Red" } };
    dict.ToString();
}

Trzy sposoby inicjalizacji tablicy

Oto one:
public void Test()
{
    int[] ar1 = { 1, 2, 3 };
    int[] ar2 = new [] { 1, 2, 3 };
    int[] ar3 = new int[] { 1, 2, 3 };
}
A czym się różnią ? Niczym:
.method public hidebysig instance void Test() cil managed
{
    .maxstack 3
    .locals init (
        [0] int32[] ar1,
        [1] int32[] ar2,
        [2] int32[] ar3)
    L_0000: nop 
    L_0001: ldc.i4.3 
    L_0002: newarr int32
    L_0007: dup 
    L_0008: ldtoken valuetype <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}/__StaticArrayInitTypeSize=12 <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}::$$method0x600009b-1
    L_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    L_0012: stloc.0 
    L_0013: ldc.i4.3 
    L_0014: newarr int32
    L_0019: dup 
    L_001a: ldtoken valuetype <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}/__StaticArrayInitTypeSize=12 <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}::$$method0x600009b-2
    L_001f: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    L_0024: stloc.1 
    L_0025: ldc.i4.3 
    L_0026: newarr int32
    L_002b: dup 
    L_002c: ldtoken valuetype <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}/__StaticArrayInitTypeSize=12 <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}::$$method0x600009b-3
    L_0031: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    L_0036: stloc.2 
    L_0037: ret 
}
Przy okazji można tutaj powiedzieć, że zawartość tych tablic trzymana jest w specjalnych globalnych zmiennych statycznych (jako ciąg bajtów). I kiedy istnieje konieczność skonstruowania takiej tablicy kopiowany jest tylko obszar pamięci. Kolejny pzykład:
public void Test(int x)
{
    int[] ar = {1, x};
    ar.ToString();
}
Kod IL:
.method public hidebysig instance void Test(int32 x) cil managed
{
    .maxstack 3
    .locals init (
        [0] int32[] ar,
        [1] int32[] CS$0$0000)
    L_0000: ldc.i4.2 
    L_0001: newarr int32
    L_0006: stloc.1 
    L_0007: ldloc.1 
    L_0008: ldc.i4.0 
    L_0009: ldc.i4.1 
    L_000a: stelem.i4 
    L_000b: ldloc.1 
    L_000c: ldc.i4.1 
    L_000d: ldarg.1 
    L_000e: stelem.i4 
    L_000f: ldloc.1 
    L_0010: stloc.0 
    L_0011: ldloc.0 
    L_0012: callvirt instance string [mscorlib]System.Object::ToString()
    L_0017: pop 
    L_0018: ret 
}
Widzimy, że w tym przypadku tworzenie zainicjalizowanej tablicy odbywa się poprzez jej utworzenie i ustawianiu indeks po indeksie jej zawartości. Podczas całego tego procesu referencja do tablicy przechowywana jest w zmiennej pomocniczej, dzięki czemu cały proces widziany z zewnątrz jest atomowy. Tak samo (inicjalizacja element po elemencie) dzieje się w przypadku tablic struktur i obiektów.

LINQ - Przyspieszenie metody Count()

Metoda Count() zlicza nam ilość elementów w enumeracji. Bardzo często chcemy sprawdzić, czy nasz zbiór zawiera określoną ilość elementów. Nie ma wtedy sensu zliczać wszystkich elementów, musimy tylko zliczyć tyle ile nam potrzeba. Oto przykład:
var gr = from n in new int[] { 1, 1, 1, 2, 2 }
         where n == 1
         select n;

bool b1 = gr.Count() == 0;
bool b2 = gr.Count() != 0;
bool b3 = gr.Count() == 2;
bool b4 = gr.Count() != 2;
bool b5 = (gr.Count() >= 2) && (gr.Count() <= 3);
Wraz ze wzrostem ilości elementów w gr całość zaczyna działać coraz wolniej. Oczywiście możemy przekształcić gr w listę lub tablice, wtedy metoda Count() będzie działać błyskawicznie, ale będzie się to wiązało z kosztem czasowym (i pamięciowym) utworzeniem takiego obiektu (umiejętne wykorzystanie metody ToList() i ToArray() aby na pewnym etapie uzyskać wynik, którego już nie trzeba będzie ponownie wyliczać, także pozwala nam na przyspieszenie wyrażeń LINQ). Próbując opierać się tylko tym co dostarcza nam LINQ, powyższy przykład możemy przyspieszyć tak:
var gr = from n in new int[] { 1, 1, 1, 2, 2 }
         where n == 1
         select n;

bool b1 = !gr.Any();
bool b2 = gr.Any();
bool b3 = gr.Take(3).Count() == 2;
bool b4 = gr.Take(3).Count() != 2;
bool b5 = (gr.Take(2).Count() == 2) && (gr.Take(4).Count() <= 3);
Pierwsze dwa testy sprowadzają się do pobrania ze zbioru tylko jednego elementu. Test b3 i b4 wymaga pobrania tylko jednego więcej elementu niż jest wymagane (metoda Take() nie zwraca trzech elementów, zwraca enumerator, który zwróci co najwyżej trzy elementy). Test b5 może być w pesymistycznym przypadku tak wolny jak poprzednio, ale generalnie powinien być szybszy. Ciągle jednak możemy go przyspieszyć rezygnując z dwukrotnej enumeracji po zbiorze za pomocą poniższej metody:
public static bool IsCountInRange<T>(this IEnumerable<T> a_enumerable, int a_min, int a_max)
{
    Debug.Assert(a_min >= 0);
    Debug.Assert(a_max >= a_min);

    int count = 0;
    IEnumerator<T> enumerator = a_enumerable.GetEnumerator();

    while (enumerator.MoveNext())
    {
        count++;
        if (count > a_max)
            return false;
    }

    return (count >= a_min);
}
Przy jej pomocy powyższy przykład możemy zapisać:
var gr = from n in new int[] { 1, 1, 1, 2, 2 }
         where n == 1
         select n;

bool b1 = !gr.Any();
bool b2 = gr.Any();
bool b3 = gr.Take(3).Count() == 2;
bool b4 = gr.Take(3).Count() != 2;
bool b5 = gr.IsCountInRange(2, 3);
Metoda IsCountInRange() sprawdza, czy ilość elementów zawiera się w podanym zakresie. W przeciwieństwie do poprzedniego testu b5 tutaj enumeracja po zbiorze dokonywana jest jednokrotnie.

2009-10-02

LINQ - uwaga na temat klauzul 'where'

W poniższym przykładzie zmierzono czas wykonywania kodu w którym występuje wiele klauzul where porównując go do kodu, w którym zastąpiono je jedną klauzulą where.
using System.Linq;
using System.Collections.Generic;

namespace SpeedTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = Enumerable.Range(0, 1000).ToList();

            {
                System.Diagnostics.Stopwatch stopwatch = 
                    new System.Diagnostics.Stopwatch();
                stopwatch.Start();

                for (int i = 0; i < 10000; i++)
                {
                    (from num in list
                     where num != 11
                     where num != 12
                     select num).ToList().ToString();
                }

                stopwatch.Stop();

                System.Console.WriteLine(("Time #1: " + 
                    stopwatch.ElapsedMilliseconds + " [ms]"));
            }

            {
                System.Diagnostics.Stopwatch stopwatch = 
                    new System.Diagnostics.Stopwatch();
                stopwatch.Start();

                for (int i = 0; i < 10000; i++)
                {
                    (from num in list
                     where num != 11
                     where num != 12
                     where num != 13
                     where num != 14
                     select num).ToList().ToString();
                }

                stopwatch.Stop();

                System.Console.WriteLine(("Time #2: " + 
                    stopwatch.ElapsedMilliseconds + " [ms]"));
            }

            {
                System.Diagnostics.Stopwatch stopwatch = 
                    new System.Diagnostics.Stopwatch();
                stopwatch.Start();

                for (int i = 0; i < 10000; i++)
                {
                    (from num in list
                     where num != 11
                     where num != 12
                     where num != 13
                     where num != 14
                     where num != 15
                     where num != 16
                     where num != 17
                     where num != 18
                     select num).ToList().ToString();
                }

                stopwatch.Stop();

                System.Console.WriteLine(("Time #3: " + 
                    stopwatch.ElapsedMilliseconds + " [ms]"));
            }

            {
                System.Diagnostics.Stopwatch stopwatch = 
                    new System.Diagnostics.Stopwatch();
                stopwatch.Start();

                for (int i = 0; i < 10000; i++)
                {
                     (from num in list
                     where (num != 11) &&
                           (num != 12)
                     select num).ToList().ToString();
                }

                stopwatch.Stop();

                System.Console.WriteLine(("Time #4: " + 
                    stopwatch.ElapsedMilliseconds + " [ms]"));
            }

            {
                System.Diagnostics.Stopwatch stopwatch = 
                    new System.Diagnostics.Stopwatch();
                stopwatch.Start();

                for (int i = 0; i < 10000; i++)
                {
                    (from num in list
                     where (num != 11) &&
                           (num != 12) &&
                           (num != 13) &&
                           (num != 14)
                     select num).ToList().ToString();
                }

                stopwatch.Stop();

                System.Console.WriteLine(("Time #5: " + 
                    stopwatch.ElapsedMilliseconds + " [ms]"));
            }

            {
                System.Diagnostics.Stopwatch stopwatch = 
                    new System.Diagnostics.Stopwatch();
                stopwatch.Start();

                for (int i = 0; i < 10000; i++)
                {
                    (from num in list
                     where (num != 11) &&
                           (num != 12) &&
                           (num != 13) &&
                           (num != 14) &&
                           (num != 15) &&
                           (num != 16) &&
                           (num != 17) &&
                           (num != 18)
                     select num).ToList().ToString();
                }

                stopwatch.Stop();

                System.Console.WriteLine(("Time #6: " + 
                    stopwatch.ElapsedMilliseconds + " [ms]"));
            }

            {
                System.Diagnostics.Stopwatch stopwatch =
                    new System.Diagnostics.Stopwatch();
                stopwatch.Start();

                for (int i = 0; i < 10000; i++)
                {
                    List<int> nums = new List<int>();

                    foreach (int num in list)
                    {
                        if ((num != 11) &&
                            (num != 12) &&
                            (num != 13) &&
                            (num != 14) &&
                            (num != 15) &&
                            (num != 16) &&
                            (num != 17) &&
                            (num != 18))
                        {
                            nums.Add(num);
                        }
                    }

                    nums.ToString();
                }

                stopwatch.Stop();

                System.Console.WriteLine(("Time #7: " +
                    stopwatch.ElapsedMilliseconds + " [ms]"));
            }

            System.Console.ReadKey();
        }
    }
}
Wyniki: Time #1: 440 [ms] Time #2: 536 [ms] Time #3: 805 [ms] Time #4: 357 [ms] Time #5: 364 [ms] Time #6: 375 [ms] Time #7: 175 [ms] Widać, że używanie wielu klauzul where ma bardzo negatywny pływ na prędkość kodu. Jeśli zależy nam na szybkim kodzie powinniśmy się ich wystrzegać. Wszystko oczywiście zależy ile takich klauzul będzie wykonywanych. W powyższmy przykładzie jest ich 10 milionów. Korzystanie z wielu klauzul to nawyk jakiego można nabyć podczas obcowania z SQL. Kod taki jest też bardziej czytelny. W ostatnim siódmym przypadku zrezygnowaliśmy z LINQ. Przyspieszenie jest znaczące (375 -> 175).

Zapmiętywanie ustawień strony dla drukowania

Poniższy fragment kodu umożliwia nam zapamiętywanie i przywracanie ustawień drukowanej strony (System.Drawing.Printing.PageSettings). Zapamiętywane są wszystkie ustawienia które możemy ustawić: rodzaj papieru, źródło papieru, marginesy, orientacja strony. Ustawienia są pamiętane w pliku konfiguracyjnym aplikacji. Prawdopodobnie dla systemów gdzie jednostką miary jest cal, poniższy kod nie będzie dobrze liczył marginesów. Przykład zastosowania:
PageSettings.Instance.Save(pageSetupDialog.PageSettings);
PageSettings.Instance.Restore(pageSetupDialog.PageSettings);
Kod:
public class Config
{
    private static Configuration s_config;

    public static Configuration Instance
    {
        get
        {
            if (s_config == null)
            {
                s_config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
            }

            return s_config;
        }
    }
}

public class PageSettings : ConfigurationSection
{
    private static String SECTION_NAME = "pegeSettings";
    private static PageSettings s_instance;

    static PageSettings()
    {
        s_instance = Config.Instance.GetSection(SECTION_NAME) as PageSettings;

        if (s_instance == null)
        {
            s_instance = new PageSettings();
            s_instance.SectionInformation.AllowExeDefinition = ConfigurationAllowExeDefinition.MachineToLocalUser;
            Config.Instance.Sections.Add(SECTION_NAME, s_instance);
        }
    }

    public static PageSettings Instance
    {
        get
        {
            return s_instance;
        }
    }

    public void Save(System.Drawing.Printing.PageSettings a_settings)
    {
        Margins margins = PrinterUnitConvert.Convert(a_settings.Margins, 
            PrinterUnit.ThousandthsOfAnInch, PrinterUnit.TenthsOfAMillimeter);
        MarginLeft = margins.Left;
        MarginRight = margins.Right;
        MarginTop = margins.Top;
        MarginBottom = margins.Bottom;

        Landscape = a_settings.Landscape;
        PaperSizeName = a_settings.PaperSize.PaperName;
        PaperSourceName = a_settings.PaperSource.SourceName;

        Config.Instance.Save();
    }

    public void Restore(System.Drawing.Printing.PageSettings a_settings)
    {
        Margins margins = new Margins(MarginLeft, MarginRight, MarginTop, MarginBottom);
        margins = PrinterUnitConvert.Convert(margins, 
            PrinterUnit.TenthsOfAMillimeter, PrinterUnit.HundredthsOfAMillimeter);
        a_settings.Margins = margins;

        a_settings.Landscape = Landscape;

        foreach (PaperSize paper_size in a_settings.PrinterSettings.PaperSizes)
        {
            if (paper_size.PaperName == PaperSizeName)
                a_settings.PaperSize = paper_size;
        }

        foreach (PaperSource printer_source in a_settings.PrinterSettings.PaperSources)
        {
            if (printer_source.SourceName == PaperSourceName)
                a_settings.PaperSource = printer_source;
        }
    }

    [ConfigurationProperty("marginLeft", DefaultValue = 10)]
    public int MarginLeft
    {
        get
        {
            return (int)base["marginLeft"];
        }
        set
        {
            base["marginLeft"] = value;
        }
    }

    [ConfigurationProperty("marginRight", DefaultValue = 10)]
    public int MarginRight
    {
        get
        {
            return (int)base["marginRight"];
        }
        set
        {
            base["marginRight"] = value;
        }
    }

    [ConfigurationProperty("marginTop", DefaultValue = 10)]
    public int MarginTop
    {
        get
        {
            return (int)base["marginTop"];
        }
        set
        {
            base["marginTop"] = value;
        }
    }

    [ConfigurationProperty("marginBottom", DefaultValue = 10)]
    public int MarginBottom
    {
        get
        {
            return (int)base["marginBottom"];
        }
        set
        {
            base["marginBottom"] = value;
        }
    }

    [ConfigurationProperty("paperSourceName", DefaultValue = "")]
    public string PaperSourceName
    {
        get
        {
            return (string)base["paperSourceName"];
        }
        set
        {
            base["paperSourceName"] = value;
        }
    }

    [ConfigurationProperty("paperSizeName", DefaultValue = "")]
    public string PaperSizeName
    {
        get
        {
            return (string)base["paperSizeName"];
        }
        set
        {
            base["paperSizeName"] = value;
        }
    }

    [ConfigurationProperty("landscape", DefaultValue = false)]
    public bool Landscape
    {
        get
        {
            return (bool)base["landscape"];
        }
        set
        {
            base["landscape"] = value;
        }
    }
}

2009-07-17

Porównywanie zawartości tablic w C#

Przykład:
{
    int[] a1 = new int[] { 145, 2, 3 };
    int[] a2 = new int[] { 145, 2, 3 };
    object[] a3 = new object[] { 145, 2, 3 };
    object[] a4 = new object[] { 145, 2, 3 };

    Debug.Assert((a1[0].Equals(a2[0])));
    Debug.Assert(a1[0] == a2[0]);

    Debug.Assert((a3[0].Equals(a4[0])));
    Debug.Assert(a3[0] != a4[0]);

    Debug.Assert((a1[0].Equals(a3[0])));
    //Debug.Assert(a1[0] == a3[0]);

    Debug.Assert((a3[0].Equals(a1[0])));
    //Debug.Assert(a3[0] == a1[0]);

    Debug.Assert(a1 != a2);
    Debug.Assert(!a1.Equals(a2));
    Debug.Assert(!Array.Equals(a1, a2));

    Debug.Assert(a3 != a4);
    Debug.Assert(!a3.Equals(a4));
    Debug.Assert(!Array.Equals(a3, a4));

    //Debug.Assert(a1 == a3);
    Debug.Assert(!a1.Equals(a3));
    Debug.Assert(!Array.Equals(a1, a3));

    //Debug.Assert(a3 == a1);
    Debug.Assert(!a3.Equals(a1));
    Debug.Assert(!Array.Equals(a3, a1));

    Debug.Assert(a1.SequenceEqual(a2));
    Debug.Assert(a3.SequenceEqual(a4));
    //Debug.Assert(a1.SequenceEqual(a3));
    //Debug.Assert(a3.SequenceEqual(a1));
}
Linie zakomentowane nie kompilują się. Jak widać obiekt typu Array, po którym dziedziczą wszystkie tablice, jak i te tablice nie przeładowują operatora ==, ani metody Equals(), dzięki temu wszystkie porównania tablic sprowadzają się do porównania referencji do nich. Sam obiekt Array nie udostępnia żadnej metody, która by nam w takim porównaniu pomogła. Więcej, samo środowisko czegoś takiego nam nie udostępnia, poza metodą SequenceEqual dostarczoną przez LINQ. Musimy więc sami zająć się sprawą porównywania tablic. Poniższy kod prezentuje 6 różnych metod porównywania tablic dla typów wartościowych i 5 dla typów referencyjnych. W metodzie Test1() porównywane są typy wartościowe. W metodzie Test2 porównywane są typy referencyjne. Dla każdej metody testowej najpierw sprawdzana jest poprawność algorytmów porównujących. A później wykonywane są dwie serie pomiarów. Najpierw duża tablica i mało sprawdzeń, później mała tablica i dużo sprawdzeń. Ilość porównań elementów tablicy jest taka sama w obu przypadkach. Chodzi tutaj o sprawdzenie kosztu samego porównania tablic.
public void Measure(int repeats, Action a)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();

    for (int i = 0; i < repeats; i++)
        a();

    sw.Stop();
    System.Console.WriteLine(sw.ElapsedMilliseconds);
}

public bool Compare1<T>(T[] ar1, T[] ar2)
{
    if (ar1.Length != ar2.Length)
        return false;

    for (int i = 0; i < ar1.Length; i++)
        if (!ar1[i].Equals(ar2[i]))
            return false;

    return true;
}

public bool Compare2<T>(T[] ar1, T[] ar2, Func<T, T, bool> comparer)
{
    if (ar1.Length != ar2.Length)
        return false;

    for (int i = 0; i < ar1.Length; i++)
        if (!comparer(ar1[i], ar2[i]))
            return false;

    return true;
}

public bool Compare3<T>(T[] ar1, T[] ar2, IEqualityComparer<T> comparer)
{
    if (ar1.Length != ar2.Length)
        return false;

    for (int i = 0; i < ar1.Length; i++)
        if (!comparer.Equals(ar1[i], ar2[i]))
            return false;

    return true;
}

public bool Compare4a(int[] ar1, int[] ar2)
{
    if (ar1.Length != ar2.Length)
        return false;

    for (int i = 0; i < ar1.Length; i++)
        if (ar1[i] != ar2[i])
            return false;

    return true;
}

public bool Compare4b(object[] ar1, object[] ar2)
{
    if (ar1.Length != ar2.Length)
        return false;

    for (int i = 0; i < ar1.Length; i++)
        if (!ar1[i].Equals(ar2[i]))
            return false;

    return true;
}

class Comparer1 : IEqualityComparer<int>
{
    public bool Equals(int x, int y)
    {
        return x == y;
    }

    public int GetHashCode(int obj)
    {
        throw new NotImplementedException();
    }
}

class Comparer2 : IEqualityComparer<object>
{
    public bool Equals(object x, object y)
    {
        return x.Equals(y);
    }

    public int GetHashCode(object obj)
    {
        throw new NotImplementedException();
    }
}

public void Test1()
{
    int[] ar1 = Enumerable.Range(1, 1000000).ToArray();
    int[] ar2 = Enumerable.Range(1, 1000000).ToArray();
    int[] ar3 = Enumerable.Range(2, 1000000).ToArray();
    int[] ar4 = Enumerable.Range(1, 1000000 - 1).ToArray();

    Debug.Assert(Compare1(ar1, ar2));
    Debug.Assert(Compare1(ar1, ar1));
    Debug.Assert(!Compare1(ar1, ar3));
    Debug.Assert(!Compare1(ar1, ar4));

    Debug.Assert(Compare2(ar1, ar2, (a, b) => (a == b)));
    Debug.Assert(Compare2(ar1, ar1, (a, b) => (a == b)));
    Debug.Assert(!Compare2(ar1, ar3, (a, b) => (a == b)));
    Debug.Assert(!Compare2(ar1, ar4, (a, b) => (a == b)));

    Debug.Assert(Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
    Debug.Assert(Compare2(ar1, ar1, (a, b) => (a.Equals(b))));
    Debug.Assert(!Compare2(ar1, ar3, (a, b) => (a.Equals(b))));
    Debug.Assert(!Compare2(ar1, ar4, (a, b) => (a.Equals(b))));

    Debug.Assert(Compare3(ar1, ar2, new Comparer1()));
    Debug.Assert(Compare3(ar1, ar1, new Comparer1()));
    Debug.Assert(!Compare3(ar1, ar3, new Comparer1()));
    Debug.Assert(!Compare3(ar1, ar4, new Comparer1()));

    Debug.Assert(Compare4a(ar1, ar2));
    Debug.Assert(Compare4a(ar1, ar1));
    Debug.Assert(!Compare4a(ar1, ar3));
    Debug.Assert(!Compare4a(ar1, ar4));

    Measure(100, () => ar1.SequenceEqual(ar2));
    Measure(100, () => Compare1(ar1, ar2));
    Measure(100, () => Compare2(ar1, ar2, (a, b) => (a == b)));
    Measure(100, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
    Measure(100, () => Compare3(ar1, ar2, new Comparer1()));
    Measure(100, () => Compare4a(ar1, ar2));

    System.Console.WriteLine("----");

    ar1 = Enumerable.Range(1, 1000000 / 100000).ToArray();
    ar2 = Enumerable.Range(1, 1000000 / 100000).ToArray();

    Measure(100 * 100000, () => ar1.SequenceEqual(ar2));
    Measure(100 * 100000, () => Compare1(ar1, ar2));
    Measure(100 * 100000, () => Compare2(ar1, ar2, (a, b) => (a == b)));
    Measure(100 * 100000, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
    Measure(100 * 100000, () => Compare3(ar1, ar2, new Comparer1()));
    Measure(100 * 100000, () => Compare4a(ar1, ar2));

    System.Console.WriteLine("----");
}

void Test2()
{
    object[] ar1 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
    object[] ar2 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
    object[] ar3 = (from n in Enumerable.Range(2, 1000000) select (object)n).ToArray();
    object[] ar4 = (from n in Enumerable.Range(1, 1000000-1) select (object)n).ToArray();

    Debug.Assert(Compare1(ar1, ar2));
    Debug.Assert(Compare1(ar1, ar1));
    Debug.Assert(!Compare1(ar1, ar3));
    Debug.Assert(!Compare1(ar1, ar4));

    Debug.Assert(Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
    Debug.Assert(Compare2(ar1, ar1, (a, b) => (a.Equals(b))));
    Debug.Assert(!Compare2(ar1, ar3, (a, b) => (a.Equals(b))));
    Debug.Assert(!Compare2(ar1, ar4, (a, b) => (a.Equals(b))));

    Debug.Assert(Compare3(ar1, ar2, new Comparer2()));
    Debug.Assert(Compare3(ar1, ar1, new Comparer2()));
    Debug.Assert(!Compare3(ar1, ar3, new Comparer2()));
    Debug.Assert(!Compare3(ar1, ar4, new Comparer2()));

    Debug.Assert(Compare4b(ar1, ar2));
    Debug.Assert(Compare4b(ar1, ar1));
    Debug.Assert(!Compare4b(ar1, ar3));
    Debug.Assert(!Compare4b(ar1, ar4));

    Measure(100, () => ar1.SequenceEqual(ar2));
    Measure(100, () => Compare1(ar1, ar2));
    Measure(100, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
    Measure(100, () => Compare3(ar1, ar2, new Comparer2()));
    Measure(100, () => Compare4b(ar1, ar2));

    System.Console.WriteLine("----");

    ar1 = (from n in Enumerable.Range(1, 1000000 / 100000) select (object)n).ToArray();
    ar2 = (from n in Enumerable.Range(1, 1000000 / 100000) select (object)n).ToArray();

    Measure(100 * 100000, () => ar1.SequenceEqual(ar2));
    Measure(100 * 100000, () => Compare1(ar1, ar2));
    Measure(100 * 100000, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
    Measure(100 * 100000, () => Compare3(ar1, ar2, new Comparer2()));
    Measure(100 * 100000, () => Compare4b(ar1, ar2));

    System.Console.WriteLine("----");
}

public void Test()
{
    Test1();
    Test2();
}
Wyniki: 2317 1891 1338 1578 1307 596 ---- 3708 1957 1531 1740 1552 730 ---- 3293 1193 2059 2300 1192 ---- 4972 1261 2151 2448 1246 ---- Najpierw może jeśli chodzi o sam koszt uruchomienia porównania tablic. Widać, że milion razy więcej wywołań przekłada się na niewielki wzrost miliona porównań. Czyli czas samego wywołania porównania jest mały. Czas ten wzrasta znacznie dla SequenceEqual(). Jeśli chodzi o czas samego porównania to jak można się było spodziewać najwolniejsza jest metoda SequenceEqual(). Dla typów wartościowych najszybsze jest porównanie bezpośrednie tablic. Wolniejsze jest wykorzystanie IEqualityComparer. Prawie tak samo szybkie jest użycie jako metody porównawczej wyrażenia lambda z bezpośrednim porównaniem wartości. Dalej jest porównanie z użyciem wyrażenia lambda, ale wykorzystujące metodę Equals(). Dalej metoda Compare1(), która także wykorzystuje Equals() do porównania. Najwolniejsza jest wspomniana SequenceEqual(). Dla typów referencyjnych jest trochę inaczej. Nie ma tutaj sensu osobno porównywać za pomocą operatora == i Equals() stąd sposobów porównania jest o jeden mniej. Najszybciej działa bezpośrednie porównanie tablic. Dalej jest metoda Compare1 wykorzystująca Equals(). Dalej jest wykorzystanie wyrażenia lambda, które wykorzystuje Equals(). Dalej wykorzystanie IEqualityComparer. Na końcu wykorzystanie SequenceEqual(). Jeśli chodzi o metody, które nadają się zarówno do porównania typów wartościowych, jak i referencyjnych to w grę wchodzi tylko wykorzystanie SequenceEqual() lub Compare1. Compare1 jest szybsze, ale za to SequenceEqual() bardziej uniwersalne (działa na enumeratorach) i dostępne w standardzie. Jeśli chodzi o prędkość to niestety czym coś jest bardziej uniwersalne to tym jest wolniejsze. Skupmy się na metodzie Compare1(). Napiszmy ją tak by stała się uniwersalną metodą rozszerzoną:
public static class ArrayExtensions
{
    public static bool SameArrays<T>(this T[] ar1, T[] ar2)
    {
        if (Object.ReferenceEquals(ar1, ar2))
            return true;

        if (ar1.Length != ar2.Length)
            return false;

        for (int i = 0; i < ar1.Length; i++)
            if (!ar1[i].Equals(ar2[i]))
                return false;

        return true;
    }
}

public void Test3()
{
    {
        int[] ar1 = Enumerable.Range(1, 1000000).ToArray();
        int[] ar2 = Enumerable.Range(1, 1000000).ToArray();
        int[] ar3 = Enumerable.Range(2, 1000000).ToArray();
        int[] ar4 = Enumerable.Range(1, 1000000 - 1).ToArray();

        Debug.Assert(ar1.SameArrays(ar2));
        Debug.Assert(ar1.SameArrays(ar1));
        Debug.Assert(!ar1.SameArrays(ar3));
        Debug.Assert(!ar1.SameArrays(ar4));
    }

    {
        object[] ar1 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
        object[] ar2 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
        object[] ar3 = (from n in Enumerable.Range(2, 1000000) select (object)n).ToArray();
        object[] ar4 = (from n in Enumerable.Range(1, 1000000 - 1) select (object)n).ToArray();

        Debug.Assert(ar1.SameArrays(ar2));
        Debug.Assert(ar1.SameArrays(ar1));
        Debug.Assert(!ar1.SameArrays(ar3));
        Debug.Assert(!ar1.SameArrays(ar4));
    }
}
Dodatkowo, dodałem tam drobną optymalizację, by nie porównywać tych samych obiektów.

Rzutowanie tablic w C#

Weźmy taki przykład dla typów wartościowych:
short[] x1 = new short[] { -34 };
ushort[] x2 = new ushort[] { 65000 };

//ushort[] x3 = (ushort[])(new short[] { -34 });
//short[] x4 = (short[])(new ushort[] { 34 });

//ushort[] x5 = (short[])x2;
//short[] x6 = (ushort[])x1;

//unchecked
//{
//    ushort[] x5 = (short[])x2;
//}

//object[] x7 = (object[])x1;

object x8 = x1;
object x9 = x2;

ushort[] x10 = (ushort[])x8;
short[] x11 = (short[])x9;

//object[] x11a = (object[])x8;
//int[] x11b = (int[])x8;

bool x12 = x8 is short[];
bool x13 = x8 is ushort[];
bool x14 = x8 is int[];

bool x14a = x8.GetType().IsArray && x8.GetType().GetElementType() == typeof(short);
bool x14b = x8.GetType().IsArray && x8.GetType().GetElementType() == typeof(ushort);

int[] x16 = new int[] { -435 };
object x17 = x16;
bool x18 = x17 is int[];
bool x19 = x17 is uint[];
bool x20 = x17 is short[];
bool x21 = x17 is long[];

uint[] x22 = (uint[])x17;

Debug.WriteLine("x12: " + x12);
Debug.WriteLine("x13: " + x13);
Debug.WriteLine("x14: " + x14);
Debug.WriteLine("x14a: " + x14a);
Debug.WriteLine("x14b: " + x14b);

Debug.WriteLine("x18: " + x18);
Debug.WriteLine("x19: " + x19);
Debug.WriteLine("x20: " + x20);
Debug.WriteLine("x21: " + x21);

Debug.WriteLine("x10[0]: " + x10[0]);
Debug.WriteLine("x11[0]: " + x11[0]);
Debug.WriteLine("x22[0]: " + x22[0]);
Zakomentowane fragmenty albo się nie skompilują, albo zwrócą wyjątek. Wynik: x12: True x13: True x14: False x14a: True x14b: False x18: True x19: True x20: False x21: False x10[0]: 65502 x11[0]: -536 x22[0]: 4294966861 Jak widzimy bezpośrednie rzutowanie jednego typu tablicy w inny typ tablicy nawet jeśli elementy tablicy są niejawnie konwertowalne nie jest możliwe. Kompilator nie pozwala na taką operację raportując ją jako błędną. Nawet jeśli spróbujemy użyć bloku unchecked. W przypadku rzutowania typów wartościowych typu: uint x = (uint)-3, kompilator także zwróci błąd. Tyle, że tutaj możemy go obejść za pomocą bloku unchecked. Poza tym rzutowanie zmiennej typu uint na zmienną typu int jest możliwe w sposób niejawny: uint x = 5; int y = (int)x. Kompilator daje nam do dyspozycji tego typu niejawne operatory konwersji. Ale już za pośrednictwem object możemy rzutować tablice int[] na uint[]. Rzutowanie elementów tablicy odbywa się tak jak byśmy stosowali (uint)int_value. Zauważmy, że np. x1 i x10 to referencjami do tego samego obiektu. W tym wypadku kompilator po prostu przestaje sprawdzać w trakcie kompilacji jak stosujemy dalej naszą zmienną typu object, Zwróćmy uwagę na wartośc zmiennych x12 i x13. Operator is nie rozróżnia pomiędzy short[], a ushort[]. Dla zmiennej x14a i x14b jest pokazane jak należy takie tablice rozróżniać, jeśli tego potrzebujemy. Rzutowanie np. pomiędzy int[] i short[] nie jest możliwe, gdyż rzutowanie nie tworzy nowej tablicy, co było by w tym wypadku potrzebne, ale tylko tworzy referencje innego typu wskazującego w to samo miejsce pamięci. Z tego samego powodu zawiedzie rzutowanie pomiędzy np. int[], a object[]. W przypadku tablic obiektów rozpatrzmy taki przykład:
class A
{
}

class B : A
{
}

{
    B[] bb = new B[] { new B() };
    A[] aa = (A[])bb;
    B[] cc = (B[])aa;
    //aa[0] = new A();
    Debug.ReferenceEquals(aa, bb);
    //Debug.Assert(bb.GetType().IsSubclassOf(aa.GetType()));
    Debug.Assert(typeof(B).IsSubclassOf(typeof(A)));
}

{
    A[] dd = new A[] { new B() };
    //B[] ee = (B[])dd;

    B[] ee = new B[1];
    Array.Copy(dd, ee, 1);
}

{
    A[] dd = new A[] { new A() };
    //B[] ee = (B[])dd;
}
Widzimy, że rzutowanie tablicy elementów typu B na tablicę elementów typu A jest możliwa. Dla takiej tablicy jest możliwe także rzutowanie powrotne. Jednakże próba wstawienia do tablicy typu A[] będącej zrzutowaną tablicą typu B[] elementu typu A zakończy się niepowodzeniem. Choć rzutowanie tablicy typu B[] na tablicę typu A[] jest możliwe to zauważmy, że tablica typu B[] nie dziedziczy po tablicy typu A[]. Rzutowanie w drugą stronę, tzn. z tablicy typu A[] na tablicę typu B[] nie jest możliwe. Oczywiście o ile nasza tablica A[] nie jest tak naprawdę tablicą typu B[]. Jednakże w tym momencie możemy się posłużyć do takiej operacji metodą Array.Copy(), która indywidualnie dla każdego kopiowanego obiektu sprawdza jego kompatybilność. Jeszcze jeden przykład:
class D
{
    public static implicit operator D(E a)
    {
        return new D();
    }
}

class E
{
    public static explicit operator E(D a)
    {
        return new E();
    }
}
{
    D[] dd = new D[] { new D() };
    //E[] ee = (E[])dd;
    E[] ee = new E[1];
    //Array.Copy(dd, ee, 1);
}

{
    D[] dd = new D[] { new D() };
    object x = dd;
    //E[] ee = (E[])x;
}

{
    E[] ee = new E[] { new E() };
    //D[] dd = (D[])ee;
    D[] dd = new D[1];
    //Array.Copy(ee, dd, 1);
}

{
    E[] ee = new E[] { new E() };
    object x = ee;
    //D[] dd = (D[])x;
}
Widzimy, że istnienie jawnego, czy niejawnego operatora konwersji wcale nie umożliwia nam konwersji tablic takich elementów. Także metoda Array.Copy() nie potrafi skorzystać z naszych operatorów konwersji. Ciekawa różnica pomiędzy tablicą typów wartościowych, a referencyjnych:
Debug.WriteLine(String.Format("{0}", new int[] { 1, 2, 3 }));
Debug.WriteLine(String.Format("{0}, {1}, {2}", new string[] { "a", "b", "c" }));
Tablica int[] nie może zostać zrzutowana na tablice object[], w przeciwieństwie do tablicy string[]. Poprzednio zostało pokazane, że niemożliwa jest rzutowanie tablicy typów wartościowych na tablicę obiektów (i odwrotnie). Jak i niemożliwe jest rzutowanie pomiędzy tablicami różnych typów wartościowych. Czy możemy problemy tego rodzaju obejść za pomocą Array.Copy() ?
{
    int[] aa = new int[] { 1, 2, 3 };
    object[] bb = new object[3];
    Array.Copy(aa, bb, 3);
    bb[0] = 567;
    Array.Copy(bb, aa, 3);

    long[] cc = new long[3];
    Array.Copy(aa, cc, 3);
    cc[0] = 456;
    //Array.Copy(cc, aa, 3);
}
Widzimy, że konwersja pomiędzy różnymi typami wartościowymi jest niemożliwa. Ale za to pakowanie typów prostych i rozpakowywanie typów referencyjnych działa z tą metodą bez problemu.

Konwersje typów prostych ze znakiem na typy bez znaku

Przykładowy kod:
{
    short x1 = 4;
    short x2 = -4;

    ushort x3 = (ushort)x1;
    ushort x4 = (ushort)x2;

    uint x5 = (uint)x1;
    uint x6 = (uint)x2;

    uint x7 = (uint)(ushort)x1;
    uint x8 = (uint)(ushort)x2;

    Debug.WriteLine(x3);
    Debug.WriteLine(x4);
    Debug.WriteLine(x5);
    Debug.WriteLine(x6);
    Debug.WriteLine(x7);
    Debug.WriteLine(x8);

}

{
    int x1 = 4;
    int x2 = -4;

    uint x3 = (uint)x1;
    uint x4 = (uint)x2;

    ulong x5 = (ulong)x1;
    ulong x6 = (ulong)x2;

    ulong x7 = (ulong)(ushort)x1;
    ulong x8 = (ulong)(ushort)x2;

    ulong x9 = (ulong)(uint)x1;
    ulong x10 = (ulong)(uint)x2;

    Debug.WriteLine(x3);
    Debug.WriteLine(x4);
    Debug.WriteLine(x5);
    Debug.WriteLine(x6);
    Debug.WriteLine(x7);
    Debug.WriteLine(x8);
    Debug.WriteLine(x9);
    Debug.WriteLine(x10);
}
Wynik: 4 65532 4 4294967292 4 65532 4 4294967292 4 18446744073709551612 4 65532 4 4294967292 W skrócie po analizie możemy powiedzieć, że dla typów ze znakiem podczas ich konwersji na typ bez znaku o większym zakresie, jeśli liczba jest ujemna, nowe najstarsze bity uzupełniane są jedynkami. Jeśli liczba jest ujemna uzupełnienie następuje zerami.

LINQ - metoda Cast()

Metoda Cast<T>() służy do konwersji obiektu typu IEnumerable<A> na IEnumerable<T>. Przykład zastosowania:
int[] x11 = new int[] { 1, 2, 3, 4, 4, 3 };
object[] x21 = x11.Cast<object>().ToArray();
int[] x31 = x21.Cast<int>().ToArray();
Cast<T>() zwraca jako wynik obiekt typu IEnumerable<T>, stąd w powyższym przykładzie potrzebne jest jeszcze wywołanie ToArray(). Ale takie coś nie zadziała:
int[] x11 = new int[] { 1, 2, 3, 4, 4, 3 };
uint[] x21 = x11.Cast<uint>().ToArray();
int[] x31 = x21.Cast<int>().ToArray();
Problem polega na tym, że Cast<T>() wykonuje konwersje tylko jeśli wszystkie obiekty kolekcji źródłowej są także obiektami typu T. Jeśli warunek nie jest spełniony dostaniemy wyjątek. Nie nastąpi to w samej metodzie Cast<T>(), dopiero próba enumeracji po zwróconym enumeratorze zakończy się wyjątkiem (efekt opóźnionego wykonywania kodu). Cast<T>() nie uwzględnia także bezpośrednich operatorów konwersji.

2009-07-09

Dwa rodzaje rzutowania w C#

W C# możemy przeprowadzić rzutowanie na dwa sposoby - poprzez użycie nawiasów albo za pomocą słówka kluczowego as. Przykład zastosowania:
public class TestClass
{
}

public void Test1()
{
 {
     TestClass t = new TestClass();
     object o = t;
     t = (TestClass)o;
 }

 {
     TestClass t = new TestClass();
     object o = t;
     t = o as TestClass;
 }
}
Różnica w kodzie pośrednim jest następująca:
.method public hidebysig instance void Test1() cil managed
{
 .maxstack 1
 .locals init (
     [0] class TestAppNET.CastingTest/TestClass t,
     [1] object o)
 L_0000: nop
 L_0001: nop
 L_0002: newobj instance void TestAppNET.CastingTest/TestClass::.ctor()
 L_0007: stloc.0
 L_0008: ldloc.0
 L_0009: stloc.1
 L_000a: ldloc.1
 L_000b: castclass TestAppNET.CastingTest/TestClass
 L_0010: stloc.0
 L_0011: nop
 L_0012: nop
 L_0013: newobj instance void TestAppNET.CastingTest/TestClass::.ctor()
 L_0018: stloc.0
 L_0019: ldloc.0
 L_001a: stloc.1
 L_001b: ldloc.1
 L_001c: isinst TestAppNET.CastingTest/TestClass
 L_0021: stloc.0
 L_0022: nop
 L_0023: ret
}
Pierwsze podejście używa rozkazu castclass, drugie isinst. Różnica w działaniu Obie metody robią to samo, różnica jest w reakcji na nieprawidłowe rzutowanie:
public class TestClass1
{
}

public class TestClass2
{
}

public void Test2()
{
 {
     TestClass1 t = new TestClass1();
     object o = t;
     TestClass2 tt = o as TestClass2;
     Debug.Assert(tt == null);
 }

 {
     TestClass1 t = new TestClass1();
     object o = t;
     bool ex = false;
     try
     {
         TestClass2 tt = (TestClass2)o;
     }
     catch (InvalidCastException)
     {
         ex = true;
     }
     Debug.Assert(ex);
 }
}
Jak widzimy operator as zwraca null jeśli konwersja się nie powiedzie, zaś rzutowanie za pomocą nawiasów zwróci wyjątek InvalidCastException. Zastosowanie dla typów wartościowych W przypadku typów wartościowych (struktury, enumeracje, typy proste), które nie mogą przyjmować wartość null użycie pierwsze metody jest niedozwolone (ściślej tylko do konwersji obiekt -> typ wartościowy, konwersje na obiekt możemy przeprowadzić zawsze bez użycia jakiejkolwiek konwersji).
public void Test3()
{
 {
     int i = 5;
     object o = i;
     //i = o as int;
 }

 {
     int i = 5;
     object o = i;
     i = (int)o;
 }
}
W przypadku typów wartościowych jest to trochę zaciemnione. Konwersja typu wartościowego na klasę bazową oznacza tak naprawdę pakowanie typu wartościowego do stworzonej wewnętrznie przez kompilator klasy. Dopiero o tej klasie możemy powiedzieć, że np. w przypadku struktur dziedziczy po ValueType. Podobnie w drugą stronę, konwersja na typ wartościowy oznacza wypakowanie obiektu do typu wartościowego. Zauważmy też, że operatorów rzutowania używamy tylko jeśli konwertujemy obiekt klasy bazowej na klasę potomną. W drugą stronę konwersja nie jest wymagana (choć użycie operatorów konwersji nie jest błędem) gdyż kompilator wie, że jest ona jak najbardziej poprawna i wykonalna. Następująca modyfikacja pierwszego przykładu nie zmieni wyglądu generowanego kodu pośredniego. Zauważmy, że konwersja na typ bazowy to zwykłe skopiowanie referencji z jednej zmiennej do drugiej.
public void Test1a()
{
 {
     TestClass t = new TestClass();
     object o = (object)t;
     t = (TestClass)o;
 }

 {
     TestClass t = new TestClass();
     object o = t as object;
     t = o as TestClass;
 }
}
Jaka jest różnica w prędkości pomiędzy tymi dwoma podejściami ? Oczywiście test ten możemy tylko przeprowadzić dla obiektów.
public class TestClass3
{
 public int A;
}

public void Test4()
{
 TestClass3 t = new TestClass3();
 object o = t;
 const int loop = 1000;

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         TestClass3 tt = o as TestClass3;
         if (tt != null)
             tt.A++;
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         TestClass3 tt = (TestClass3)o;
         if (tt != null)
             tt.A++;
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         try
         {
             TestClass3 tt = (TestClass3)o;
             if (tt != null)
                 tt.A++;
         }
         catch (InvalidCastException)
         {
             Debug.Assert(false);
         }

     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         TestClass2 tt = o as TestClass2;
         if (tt == null)
             t.A++;
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         try
         {
             TestClass1 tt = (TestClass1)o;
             Debug.Assert(true);
         }
         catch (InvalidCastException)
         {
             if (t != null)
                 t.A++;
         }
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 System.Console.WriteLine(t.A);
}
Wyniki dla zakomentowanego ostatniego pomiaru: 958 1045 992 1242 0 800000000 Odkomentowany ostatni pomiar, zmniejszono liczbę pętli do loop = 1000: 0 0 0 0 2567 5000 Widzimy, że pomiędzy tymi metodami nie ma zbytniej różnicy w prędkości. Dopóki rzutowanie nie wywoła wyjątku. Jeśli to ma być nasz sposób na sprawdzanie typu obiektu to lepiej z niego zrezygnować. Można powiedzieć, że detekcja niepoprawności typu za pomocą () jest 2500 razy wolniejsza niż za pomocą operatora as. Konwersje pomiędzy typami standardowymi
public void Test5()
{
 int i1 = 5;

 double d1 = 5;
 double d2 = 5.5;
 double d3 = i1;
 double d4 = (double)i1;

 int i2 = (int)5.5;
 int i3 = (int)d1;

 i1.ToString();
 i2.ToString();
 i3.ToString();
 d1.ToString();
 d2.ToString();
 d3.ToString();
 d4.ToString();
}
Kod pośredni:
.method public hidebysig instance void Test5() cil managed
{
 .maxstack 1
 .locals init (
     [0] int32 i1,
     [1] float64 d1,
     [2] float64 d2,
     [3] float64 d3,
     [4] float64 d4,
     [5] int32 i2,
     [6] int32 i3)
 L_0000: nop
 L_0001: ldc.i4.5
 L_0002: stloc.0
 L_0003: ldc.r8 5
 L_000c: stloc.1
 L_000d: ldc.r8 5.5
 L_0016: stloc.2
 L_0017: ldloc.0
 L_0018: conv.r8
 L_0019: stloc.3
 L_001a: ldloc.0
 L_001b: conv.r8
 L_001c: stloc.s d4
 L_001e: ldc.i4.5
 L_001f: stloc.s i2
 L_0021: ldloc.1
 L_0022: conv.i4
 L_0023: stloc.s i3
 L_0025: ldloca.s i1
 L_0027: call instance string [mscorlib]System.Int32::ToString()
 L_002c: pop
 L_002d: ldloca.s i2
 L_002f: call instance string [mscorlib]System.Int32::ToString()
 L_0034: pop
 L_0035: ldloca.s i3
 L_0037: call instance string [mscorlib]System.Int32::ToString()
 L_003c: pop
 L_003d: ldloca.s d1
 L_003f: call instance string [mscorlib]System.Double::ToString()
 L_0044: pop
 L_0045: ldloca.s d2
 L_0047: call instance string [mscorlib]System.Double::ToString()
 L_004c: pop
 L_004d: ldloca.s d3
 L_004f: call instance string [mscorlib]System.Double::ToString()
 L_0054: pop
 L_0055: ldloca.s d4
 L_0057: call instance string [mscorlib]System.Double::ToString()
 L_005c: pop
 L_005d: ret
}
Jak widać tutaj do konwersji używany jest rozkaz conv z różnymi sufiksami. Konwersje pośrednie:
public class TestClass4
{
 public static explicit operator TestClass4(int i)
 {
     return new TestClass4();
 }

 public static explicit operator  int(TestClass4 t)
 {
     return 11;
 }
}

public struct TestStruct
{
 public static explicit operator TestStruct(int i)
 {
     return new TestStruct();
 }

 public static explicit operator int(TestStruct t)
 {
     return 11;
 }
}

public void Test6()
{
 TestClass4 t1 = new TestClass4();
 int i1 = (int)t1;
 t1 = (TestClass4)i1;

 TestStruct t2 = new TestStruct();
 int i2 = (int)t2;
 t2 = (TestStruct)i2;
}
Kod pośredni:
.method public hidebysig instance void Test6() cil managed
{
 .maxstack 1
 .locals init (
     [0] class TestAppNET.CastingTest/TestClass4 t1,
     [1] int32 i1,
     [2] valuetype TestAppNET.CastingTest/TestStruct t2,
     [3] int32 i2)
 L_0000: nop
 L_0001: newobj instance void TestAppNET.CastingTest/TestClass4::.ctor()
 L_0006: stloc.0
 L_0007: ldloc.0
 L_0008: call int32 TestAppNET.CastingTest/TestClass4::op_Explicit(class TestAppNET.CastingTest/TestClass4)
 L_000d: stloc.1
 L_000e: ldloc.1
 L_000f: call class TestAppNET.CastingTest/TestClass4 TestAppNET.CastingTest/TestClass4::op_Explicit(int32)
 L_0014: stloc.0
 L_0015: ldloca.s t2
 L_0017: initobj TestAppNET.CastingTest/TestStruct
 L_001d: ldloc.2
 L_001e: call int32 TestAppNET.CastingTest/TestStruct::op_Explicit(valuetype TestAppNET.CastingTest/TestStruct)
 L_0023: stloc.3
 L_0024: ldloc.3
 L_0025: call valuetype TestAppNET.CastingTest/TestStruct TestAppNET.CastingTest/TestStruct::op_Explicit(int32)
 L_002a: stloc.2
 L_002b: ret
}
Jak widać podobieństwo jest tylko na poziomie składni. W kodzie pośrednim konwersja jest dokonywana zupełnie inaczej. Za pomocą operatora as nie da się przeprowadzić tego typu konwersji.

2009-06-12

DebuggerTypeProxyAttribute

Atrybut ten może zostać nałożony na klasę, strukturę i zestaw. 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 mówi debugerowi by podczas debugowania danej klasy pokazał nam zawartość innej klasy. Jedynym wymaganiem względem tej klasy jest by istniał jej konstruktor przyjmujący jako parametr obiekt klasy, którą chcemy substytuować. Przykład zastosowania:
public class List
{
  public class Node
  {
      [System.Diagnostics.DebuggerBrowsable(
          System.Diagnostics.DebuggerBrowsableState.Never)]
      private Node m_next;

      public readonly int Value;

      public Node(int a_value)
      {
          Value = a_value;
      }

      public Node Next
      {
          get
          {
              return m_next;
          }
      }

      protected internal void Add(Node a_node)
      {
          if (m_next == null)
              m_next = a_node;
          else
              m_next.Add(a_node);
      }
  }

  [System.Diagnostics.DebuggerBrowsable(
      System.Diagnostics.DebuggerBrowsableState.Never)]
  private Node m_head;

  public void Add(Node a_node)
  {
      if (m_head == null)
          m_head = a_node;
      else
          m_head.Add(a_node);
  }

  public Node Head
  {
      get
      {
          return m_head;
      }
  }
}

public class DebuggerTypeProxyTest
{
  public void Test()
  {
      List list = new List();

      list.Add(new List.Node(5));
      list.Add(new List.Node(6));
      list.Add(new List.Node(7));

      list.ToString();
  }
}
Podgląd list wygląda tak: Po uzupełnieniu kodu odpowiednio:
public class ListProxy
{
  private List m_list;

  public ListProxy(List a_list)
  {
      m_list = a_list;
  }

  private IEnumerable<int> getEnum()
  {
      List.Node node = m_list.Head;

      while (node != null)
      {
          List.Node n = node;
          node = node.Next;
          yield return n.Value;
      }
  }

  [System.Diagnostics.DebuggerBrowsable(
      System.Diagnostics.DebuggerBrowsableState.RootHidden)]
  public int[] List
  {
      get
      {
          return getEnum().ToArray();
      }
  }
}

[System.Diagnostics.DebuggerTypeProxy(typeof(ListProxy))]
public class List
{
  public class Node
  {
      [System.Diagnostics.DebuggerBrowsable(
          System.Diagnostics.DebuggerBrowsableState.Never)]
      private Node m_next;

      public readonly int Value;

      public Node(int a_value)
      {
          Value = a_value;
      }

      public Node Next
      {
          get
          {
              return m_next;
          }
      }

      protected internal void Add(Node a_node)
      {
          if (m_next == null)
              m_next = a_node;
          else
              m_next.Add(a_node);
      }
  }

  [System.Diagnostics.DebuggerBrowsable(
      System.Diagnostics.DebuggerBrowsableState.Never)]
  private Node m_head;

  public void Add(Node a_node)
  {
      if (m_head == null)
          m_head = a_node;
      else
          m_head.Add(a_node);
  }

  public Node Head
  {
      get
      {
          return m_head;
      }
  }
}

public class DebuggerTypeProxyTest
{
  public void Test()
  {
      List list = new List();

      list.Add(new List.Node(5));
      list.Add(new List.Node(6));
      list.Add(new List.Node(7));

      list.ToString();
  }
}
W debugerze zobaczymy: Dzięki użyciu tego atrybutu możemy kompleksowo zmienić sposób pokazywania zawartości klasy podczas debugowania. Co ważniejsze dane możemy wizualizować zupełnie inaczej niż wyglądają w strukturze obiektu. Jeśli obiekt implementuje strukturę listę (jak w powyższym przykładzie), możemy je pokazać jako tablicę, nie zmuszając użytkownika do klikania poprzez wszystkie elementy listy. Możemy zmienić wizualizację naszych klas, a także klas z biblioteki .NET (jak i dowolnie innej biblioteki). Możemy też powiedzieć, że atrybut ten pozwala nam rozdzielić kod dla debugera od kodu klasy. Jeśli klasa proxy musi się dostać do składników prywatnych klasy debugowanej najlepiej zdefiniować ją jako klasę wewnętrzną klasy debugowanej. Reszta informacji dla tego atrybutu podaje w postaci pytań i odpowiedzi. Jak można się przekonać niżej czasami coś nie działa, czasami działa inaczej niż powinno. Chyba wszystkie te sytuacje podane niżej kiedy coś nie działa, a powinno, można zakwalifikować jako błędy. Jak nałożyć atrybut na zestaw ? Podobnie jak dla DebugerDisplayAttribute. Więcej szczegółów można znaleźć w tamtym wpisie. Przykład:
[assembly: System.Diagnostics.DebuggerTypeProxy(typeof(TestAppNET.Priv.TestClassProxy), Target = typeof(TestAppNET.TestClass))]
Czy klasa proxy może być klasą wewnętrzną ? Może. Czy klasa proxy może zostać zastosowana dla klasy debugowanej generycznej? Tak. Przykład:
public class TestClassProxy<K,V>
{
  public K x;
  public V y;
  public string str;

  public TestClassProxy(TestClass<K,V> a_tc)
  {
      x = a_tc.x;
      y = a_tc.y;
      str = "test " + a_tc.str;
  }
}

[System.Diagnostics.DebuggerTypeProxy(typeof(TestAppNET.TestClassProxy<,>))]
public class TestClass<K,V>
{
  public K x;
  public V y;
  public string str;
}
Dobrze jest zwrócić uwagę jak definiuje się typ generyczny w atrybucie DebuggerTypeProxy. Teraz jeśli zamiast typu w atrybucie podamy stringa przykład przestanie działać:
[System.Diagnostics.DebuggerTypeProxy("TestAppNET.TestClassProxy<,>")]
Proxy przestanie działać też dla poniższego przykładu kiedy zamkniemy typ generyczny dla proxy:
public class TestClassProxy
{
  public int x;
  public string y;
  public string str;

  public TestClassProxy(TestClass a_tc)
  {
      x = a_tc.x;
      y = a_tc.y;
      str = "test " + a_tc.str;
  }
}

public class BaseClass<K, V>
{
  public K x;
  public V y;
}

[System.Diagnostics.DebuggerTypeProxy("TestAppNET.TestClassProxy")]
public class TestClass : BaseClass<int, string>
{
  public string str;
}
Jak musi być widoczność klasy proxy ? Nie ma znaczenia. Oczywiście podając klasy debugowaną jako type musi ona być widoczna. Podając ją jako string należy zwrócić uwagę, że jeśli klasa proxy jest klasą wewnętrzną w jej pełnej nazwie dla składników klasy wewnętrznej zamiast kropki używamy plusa:
[System.Diagnostics.DebuggerTypeProxy("TestAppNET.SomeClass+InnerTestClassProxy")]
Dla atrybutu dla zestawu kiedy zamiast typu użyłem stringa proxy przestało działać (niezależnie dla którego parametru konstruktora atrybutu). Jaka musi być widoczność klasy debugowanej przy definiowaniu atrybutu dla zestawu ? Zgodnie z tym co powiedziałem w powyżej musi być widoczna, by móc ją podać jako typ. Czy klasa proxy może dziedziczyć po innej klasie, implementować interfejsy ? Nie ma problemu. Czy jeśli klasa proxy dziedziczy po innej klasie to czy jej pola i właściwości też są pokazywane podczas debugowania ? Tak, w rozwinięciu klasy. Co jeśli proxy nałożymy na klasę bazową klasy, której podgląd będziemy chcieli zobaczyć ? Proxy dla tej klasy zostanie uruchomione. Co jeśli konstruktor proxy jako parametru wymaga klasy bazowej naszej debugowanej klasy lub interfejsu, który nasza klasa debugowana implementuje ? Jeśli takie proxy jest nałożone na naszą klasę, podczas debugowania klasy proxy zostanie użyta. Czy możemy nasza klasę proxy wykorzystać dla dwóch różnych klas debugowanych ? Czyli nasza klasa proxy posiada dwa konstruktory dla dwóch różnych klas debugowanych. Możemy tak zrobić. Czy składniki prywatne klasy proxy są wyświetlane ? Nie, pokazywane są tylko składniki publiczne i protected (pola i właściwości). Co się stanie kiedy wpis w atrybucie DebuggerTypeProxyAttribute jest błędny ? Nic się nie dzieje, nie ma żadnego komunikatu o błędzie. Czy po rekompilacji kodu w którym znajduje się klasa proxy, by efekt był widoczny, a atrybut został nałożony na zestaw, trzeba zrestartować VS ? Nie. Dla ścisłości atrybut na zestaw został już nałożony, a VS zrestartowane, teraz tylko rekompilujemy klasę proxy. Czy klasa proxy i klasa debugowana mogą się znajdować w innych zestawach ? Mogą, ale aby uniknąć circular references, w innym zestawie może się znajdować tylko debugowana klasa. Po umieszczeniu tam proxy kodu nie uda nam się tego skompilować. Co jeśli dla klasy debugowanej są ustanowione jednocześnie proxy w zestawie i klasie debugowanej ? W przeciwieństwie do tego jak działa atrybut DebugerDisplayAttribute tutaj proxy zdefiniowane w klasie debugowanej zostanie zignorowane. Co jeśli zestaw z klasa proxy załadujemy dynamicznie ? Załadować się załadował, ale debug nie uwzględnił klasy proxy. Co jeśli klasa debugowana jest w innym zestawie ? Nie ma żadnego problemu o ile nie jest ładowana dynamicznie. Przy ładowaniu dynamicznym klasa proxy się nie skompiluje z powodu braku referencji do klasy debugowanej. Patrz pytanie wyżej (klasa proxy nie może być ładowana dynamicznie). Jak debugować klasę proxy ? Można wyświetlać okienka, wydawać dźwięki, logować do pliku. Nie udało mi się skorzystać z konsoli VS dostępnej w trybie debug. Kiedy tworzony jest obiekt klasy proxy ? Dla Watch, Autos, Locals dla każdego wymaganego odświeżenia wartości (krok debugera albo zatrzymanie wykonywania programu), o ile jest rozwinięty. Dla podglądy stworzenie obiektu proxy następuje w momencie rozwinięcia obiektu. Ponieważ obiekt klasy proxy tworzony jest za każdym razem należy zwrócić uwagę na szybkość działania klasy proxy. Co się dzieje kiedy na klasę proxy nałożymy jakieś atrybuty modyfikujące wyświetlanie zawartości klasy w debugerze ? Dla klasy proxy DebugerDisplayAttribute nie jest uwzględniany. To samo tyczy się zastosowania metody ToString() do pokazywania informacji o obiekcie podczas debugowania. Więcej informacji można znaleźć przy opisie atrybutu DebugerDisplayAttribute. Atrybut DebuggerBrowsable działa.

2009-06-08

.NET 3.5, Java, Visual Studio 2008 C++ - prędkość działania na tablicach

Testy były robione na 64 bitowej Viście. Testy C++ na Visual Studio 2008 SP1. Testy C# na Visual Studio 2008 SP1 (.NET 3.5). Testy Javy na JRE 1.6.0 (IDE to Netbeans). Wszystkie programy kompilowałem w Release (o ile była taka możliwość). Wszystkie programy były uruchamiane samodzielnie, nie z IDE. Java i C++ nie wykazywały spowolnienia podczas uruchamiania z IDE. C# w pierwszym teście dwie pierwsze metody potrafił wykonać 4 razy wolniej. Mając na myśli C++ mam na myśli język niezarządzalny, bez Garbage Collectora. Dla precyzyjnego pomiaru czasu w C++ wykorzystałem klasę CPreciseTimer. C#, .NET 3.5, kompilator do kodu pośredniego, kompilator kodu pośredniego do maszynowego - mam nadzieję, że wiadomo jakie są różnicę, bo dalej nie będę taki precyzyjny (chyba już nawet w tytule postu nie jestem). Podobnie zresztą można powiedzieć o Javie. W przypadku C++ testy były robione podczas kompilacji na 32 i 64 bity. To samo tyczy się C#. Tutaj kod pośredni wyglądał identycznie (przynajmniej dla pierwszego testu), różnica brała się pewnie podczas kompilacji kodu pośredniego. W Javie kompilacja jest chyba zawsze na 32 bity. Podczas testów wyłączałem oszczędzanie energii polegające na spowolnieniu częstotliwości procesora kiedy nic się nie dzieje. Dostęp do dużej tablicy Test polega na stworzeniu wielkiej tablicy, zapełnieniu ją losowymi wartościami i zsumowaniu ich w prostej pętli. Tablica składa się z 100 milionów intów. Tablica jest na tyle duża by pominąć efekt pamięci cache procesora na jej przetwarzanie. Java:
public class ArraySpeedTest1
{
    private int[] big_array;

    public ArraySpeedTest1()
    {
        big_array = new int[1000*1000*100];
        java.util.Random r = new java.util.Random();
        for (int i = 0; i < big_array.length; i++)
            big_array[i] = r.nextInt(5);
    }

    private int SpeedTest1()
    {
        int r = 0;

        for (int i = 0; i < big_array.length; i++)
            r += big_array[i];

        return r;
    }

    private int SpeedTest2()
    {
        int r = 0;

        for (int n : big_array)
            r += n;

        return r;
    }

    public void Test()
    {
        long t = System.nanoTime();
        int r = SpeedTest1();
        t = System.nanoTime() - t;
        System.out.println(String.format("SpeedTest1, sum: %d, time: %d [ms]", r, t / 1000000));

        t = System.nanoTime();
        r = SpeedTest2();
        t = System.nanoTime() - t;
        System.out.println(String.format("SpeedTest2, sum: %d, time: %d [ms]", r, t / 1000000));
    }
}
Wynik: SpeedTest1, sum: 199998357, time: 244 [ms] SpeedTest2, sum: 199998357, time: 168 [ms] C++:
class ArraySpeedTest1
{
    private: int BIG_ARRAY_LENGTH;
    private: int* big_array;

    public: ArraySpeedTest1()
    {
        BIG_ARRAY_LENGTH = 1000*1000*100;

        big_array = new int[BIG_ARRAY_LENGTH];

        srand((unsigned)time(NULL));
        for (int i=0; i<BIG_ARRAY_LENGTH; i++)
            big_array[i] = rand() % 4;
    }

    public: ~ArraySpeedTest1()
    {
        delete big_array;
    }

    private: int SpeedTest1()
    {
        int r = 0;
        int* p = big_array;
        for (int i=0; i<BIG_ARRAY_LENGTH; i++)
            r += *p++;
        return r;
    }

    private: int SpeedTest2()
    {
        int r = 0;
        for (int i=0; i<BIG_ARRAY_LENGTH; i++)
            r = r + big_array[i];
        return r;
    }

    public: void Test()
    {
        CPreciseTimer* timer = new CPreciseTimer();
        timer->StartTimer();
        int r = SpeedTest1();
        timer->StopTimer();
        std::cout << "SpeedTest1, sum: " << r << ", time: " << timer->GetTime() / 1000 << "[ms]\r\n";
        delete timer;

        timer = new CPreciseTimer();
        timer->StartTimer();
        r = SpeedTest2();
        timer->StopTimer();
        std::cout << "SpeedTest2, sum: " << r << ", time: " << timer->GetTime() / 1000 << "[ms]\r\n";
        delete timer;
    }
};
Wyniki: 32 bity: SpeedTest1, sum: 449948271, time: 82[ms] SpeedTest2, sum: 449948271, time: 82[ms] 64 bity: SpeedTest1, sum: 449948271, time: 82[ms] SpeedTest2, sum: 449948271, time: 82[ms] C#:
public class ArraySpeedTest1
{
    private int[] big_array = new int[1000 * 1000 * 100];

    public ArraySpeedTest1()
    {
        Random r = new Random();
        for (int i = 0; i < big_array.Length; i++)
            big_array[i] = r.Next(5);
    }

    private int SpeedTest1()
    {
        int r = 0;

        for (int i = 0; i < big_array.Length; i++)
            r += big_array[i];

        return r;
    }

    private int SpeedTest2()
    {
        int r = 0;

        foreach (int n in big_array)
            r += n;

        return r;
    }

    private int SpeedTest3()
    {
        return big_array.Sum();
    }

    public void Test()
    {
        Action<Func<int>, string> d = (f, s) =>
        {
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            int r = f();
            sw.Stop();
            System.Console.WriteLine("{0}, sum: {1}, time: {2} [ms]", s, r, sw.ElapsedMilliseconds);
        };

        d(SpeedTest1, "SpeedTest1");
        d(SpeedTest2, "SpeedTest2");
        d(SpeedTest3, "SpeedTest3");
    }
}
Wyniki 32 bity: SpeedTest1, sum: 199995106, time: 111 [ms] SpeedTest2, sum: 199995106, time: 105 [ms] SpeedTest3, sum: 199995106, time: 1064 [ms] 64 bity: SpeedTest1, sum: 199981552, time: 130 [ms] SpeedTest2, sum: 199981552, time: 134 [ms] SpeedTest3, sum: 199981552, time: 1028 [ms] Komentarz: W C# kod 32 bitowy jest wykonywany szybciej niż kod 64 bitowy. W C++ nie widać różnicy w prędkości pomiędzy kodem 64 bitowym, a 32 bitowym. Wygląda na to, że optymalizacja dla 64 bitowego kodu nie jest najlepsza dla C#. Co do różnic w prędkości widzimy, że kod najszybciej wykonuje się w C++ (obie wersje tak samo szybkie). Dalej jest C#. A na końcu Java (co ciekawe wersja z wykorzystaniem enumeracji jest wyraźnie szybsza). Dla C# wersja z wykorzystaniem LINQ choć ma najelegantszy kod to jest najwolniejsza. Dla C++ widzimy, że ręczna zabawa na wskaźnikach nie zawsze daje efekty. Możemy uśrednić wyniki: C++: 85 [ms], C#: 115[ms], Java: 200[ms] Wielokrotny dostęp do małej tablicy Ilość sumowanych wartości jest taka sama, tylko, że zamiast sumować tablice 100 milionów elementów, sumujemy 100 tysięcy razy tablice 1000 elementów. Dzięki temu korzystamy w pełni z pamięci cache procesora. Java:
public class ArraySpeedTest2
{
    private int OUTER_LOOP;
    private int[] big_array;

    public ArraySpeedTest2()
    {
        OUTER_LOOP = 1000*100;
        big_array = new int[1000];
        java.util.Random r = new java.util.Random();
        for (int i = 0; i < big_array.length; i++)
            big_array[i] = r.nextInt(5);
    }

    private int SpeedTest1()
    {
        int r = 0;

        for (int j=0; j<OUTER_LOOP; j++)
            for (int i = 0; i < big_array.length; i++)
                r += big_array[i];

        return r;
    }

    private int SpeedTest2()
    {
        int r = 0;

        for (int j=0; j<OUTER_LOOP; j++)
            for (int n : big_array)
                r += n;

        return r;
    }

    public void Test()
    {
        long t = System.nanoTime();
        int r = SpeedTest1();
        t = System.nanoTime() - t;
        System.out.println(String.format("SpeedTest1, sum: %d, time: %d [ms]", r, t / 1000000));

        t = System.nanoTime();
        r = SpeedTest2();
        t = System.nanoTime() - t;
        System.out.println(String.format("SpeedTest2, sum: %d, time: %d [ms]", r, t / 1000000));
    }
}
Wyniki: SpeedTest1, sum: 201900000, time: 224 [ms] SpeedTest2, sum: 201900000, time: 136 [ms] C++:
class ArraySpeedTest2
{
    private: int OUTER_LOOP;
    private: int BIG_ARRAY_LENGTH;
    private: int* big_array;

    public: ArraySpeedTest2()
    {
        OUTER_LOOP = 1000*100;
        BIG_ARRAY_LENGTH = 1000;

        big_array = new int[BIG_ARRAY_LENGTH];

        srand((unsigned)time(NULL));
        for (int i=0; i<BIG_ARRAY_LENGTH; i++)
            big_array[i] = rand() % 4;
    }

    public: ~ArraySpeedTest2()
    {
        delete big_array;
    }

    private: int SpeedTest1()
    {
        int r = 0;
        
        int* p;
        for (int j=0; j<OUTER_LOOP; j++)
        {
            p = big_array;
            for (int i=0; i<BIG_ARRAY_LENGTH; i++)
                r += *p++;
        }
        return r;
    }

    private: int SpeedTest2()
    {
        int r = 0;
        for (int j=0; j<OUTER_LOOP; j++)
            for (int i=0; i<BIG_ARRAY_LENGTH; i++)
                r = r + big_array[i];
        return r;
    }

    public: void Test()
    {
        CPreciseTimer* timer = new CPreciseTimer();
        timer->StartTimer();
        int r = SpeedTest1();
        timer->StopTimer();
        std::cout << "SpeedTest1, sum: " << r << ", time: " << timer->GetTime() / 1000 << "[ms]\r\n";
        delete timer;

        timer = new CPreciseTimer();
        timer->StartTimer();
        r = SpeedTest2();
        timer->StopTimer();
        std::cout << "SpeedTest2, sum: " << r << ", time: " << timer->GetTime() / 1000 << "[ms]\r\n";
        delete timer;
    }
};
Wyniki: 32 bity: SpeedTest1, sum: 155800000, time: 45[ms] SpeedTest2, sum: 155800000, time: 45[ms] 64 bity: SpeedTest1, sum: 155800000, time: 45[ms] SpeedTest2, sum: 155800000, time: 45[ms] C#:
public class ArraySpeedTest2
{
    private int OUTER_LOOP = 1000 * 100;
    private int[] big_array = new int[1000];

    public ArraySpeedTest2()
    {
        Random r = new Random();
        for (int i = 0; i < big_array.Length; i++)
            big_array[i] = r.Next(5);
    }

    private int SpeedTest1()
    {
        int r = 0;

        for (int j = 0; j < OUTER_LOOP; j++)
            for (int i = 0; i < big_array.Length; i++)
                r += big_array[i];

        return r;
    }

    private int SpeedTest2()
    {
        int r = 0;

        for (int j = 0; j < OUTER_LOOP; j++)
            foreach (int n in big_array)
                r += n;

        return r;
    }

    private int SpeedTest3()
    {
        int r = 0;

        for (int j = 0; j < OUTER_LOOP; j++)
            r += big_array.Sum();

        return r;
    }

    public void Test()
    {
        Action<Func<int>, string> d = (f, s) =>
        {
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            int r = f();
            sw.Stop();
            System.Console.WriteLine("{0}, sum: {1}, time: {2} [ms]", s, r, sw.ElapsedMilliseconds);
        };

        d(SpeedTest1, "SpeedTest1");
        d(SpeedTest2, "SpeedTest2");
        d(SpeedTest3, "SpeedTest3");
    }
}
Wyniki: 32 bity: SpeedTest1, sum: 195800000, time: 93 [ms] SpeedTest2, sum: 195800000, time: 90 [ms] SpeedTest3, sum: 195800000, time: 1051 [ms] 64 bity: SpeedTest1, sum: 200200000, time: 117 [ms] SpeedTest2, sum: 200200000, time: 116 [ms] SpeedTest3, sum: 200200000, time: 1013 [ms] Widzimy, że kod w C++ przyspieszył dwukrotnie, zaś kod w C# i w Javie przyspieszył nieznacznie. Wynika to prawdopodobnie z ilości instrukcji procesora na pojedyńczą pętle. Kiedy cała pętla zaczyna korzystać z pamięci cache, do której dostęp jest szybszy niż do pamięci zewnętrznej, to kod w C++, który ma najmniej instrukcji na pętle zaczyna działać najszybciej. Prawdopodobnie jest tak, że w C++ dla pierwszego testu to procesor czeka na dane z pamięci, a wykonywanie kodu stoi. W C# i Javie nie widzimy tak wielkiego przyspieszenia, gdyż pomimo dostępu do pamięci cache procesor i tak ma dużo rozkazów do przetworzenia. Możemy uśrednić wyniki: C++: 45 () [ms], C#: 90[ms], Java: 170[ms]. Te same testy uruchomione na domowym komputerze dla C++ i C# przyspieszyły do 30 [ms] i 60 [ms]. Stosunek przyspieszenia jest idealnie proporcjonalny do prędkości zegara procesora. Dla porównania wersja pierwsza działająca na dużej tablicy w ogóle nie przyspieszyła. Widzimy, że kiedy dane są w pamięci cache liczy się tylko prędkość procesora. W pierwszym przypadku prawdopodobnie znaczenie ma prędkość pamięci (u mnie w obu przypadkach taka sama patrząc na indeks szybkości liczony przez Viste). Kod w C++ wykonał się w drugim przypadku w 45 [ms]. Prędkość procesora to 2,26 [GHz]. W tym czasie przetworzył 100M elementów tablicy. Ile elementów tablicy przetworzył by w sekundę? - 2222M. Czyli możemy powiedzieć, że efektywna częstotliwość przetwarzania tablicy to 2,22 [GHz]. A taka pętla to na pewno jest kilka instrukcji procesora. Tutaj widać jak działają współczesne procesory, jak potrafią optymalizować wykonanie kodu. I widać (może trochę mniej), jak skomplikowanym zagadnieniem jest optymalizacja kodu pod współczesne procesory - czym na niższy poziom schodzimy tym efekty naszego niby-przyspieszania mogą być coraz bardziej zaskakujące. Potrzebna jest tutaj dokładna wiedza jak działa procesor niestety...

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.