08.02.2012 13:05

Zvláštnosti jazyka C#

V jednom článku na stackoverflow jsem našel zajímavou diskuzi ohledně různých případů v C#, kdy se aplikace chová jinak, než jak by se zpočátku očekávalo. Rozhodl jsem se s vámi o tyto zajímavosti podělit.

Neočekávaná NullReferenceException

Máme tuto metodu:

static void Foo<T>() where T : new()
{
    T t = new T();
    Console.WriteLine(t.ToString());
    Console.WriteLine(t.GetHashCode());
    Console.WriteLine(t.Equals(t));
    
    Console.WriteLine(t.GetType());//NullReferenceException
}

Tato metoda požaduje jako typový parametr něco, co má veřejný konstruktor bez parametrů (omezení new()). První čtyři řádky proběhnou bez problémů, ale na posledním vyskočí NullReferenceException: Odkaz na objekt není nastaven na instanci objektu. Tahle vyjímká vás určite už taky párkrát potrápila, co ale dělá tady? Vytvoříme-li instanci typu T, instance přece nemůže být null. Nebo ano? Pokud na to nepřijdete, povím vám to o pár řádků níže.














Tak co, přišli jste na něco? Odpověď zní: Jakýkoliv typ Nullable<T>. Proč? Všechny metody ToString, GetHashCode i Equals jsou virtuální, dají se atributem override přepisovat v implementaci podřazeného typu. Struktura Nullable<T> má všechny tyto tři metody přepsané. Ale metoda GetType není virtuální. Z toho důvodu je její implementace jen v typu System.Object, a proto se musí instance zaboxovat, tzn. přetypovat na (object)t. Nullable<T> má tu zvláštnost, že pokud se zaboxuje a nemá hodnotu, operace zaboxování vrátí null. Proto voláme vlastně null.GetType(), což vyhodí vyjímku.

Null podruhé

S menší úpravou dostaneme tuto metodu:

static void Foo2<T>() where T : class, new()
{
    T t = new T();
    Console.WriteLine(t.ToString());//NullReferenceException
}

Tady už to bude složitější. Ta metoda chce jen třídy, takže Nullable nepřipadá v úvahu. Vyjímku to má přece vyhodit jen tehdy, když t je null. Ale to je instance, každá nová instance je unikátní a nemůže být nulové reference. Že by to šlo? Pomůže nám „magie“ v System.Runtime.Remoting.Proxies.

class NullProxyAttribute : System.Runtime.Remoting.Proxies.ProxyAttribute
{
    public override MarshalByRefObject CreateInstance(Type serverType)
    {
        return null;
    }
}

[NullProxy]
class NullType : ContextBoundObject
{}

Naše třída NullType dědí ContextBoundObject a s ním i MarshalByRefObject, díky čemuž má trochu odlišné spravování, zvláště mezi vzdálenými doménami. Kvůli atributu je třída jen proxy a jeho metoda CreateInstance vrátí nulovou referenci. Toto je čisté zlo, nepoužívejte to. Maximálně s tím můžete trápit kolegy.

Není this jako this

Schválně, přijdete na to, jak zkompilovat tento kód?
public void Foo()
{
    this = new A();
}

Otázka zní: Jak může přiřazovat třída sama sebe? Třída nemůže. Ale struktura ano!

public struct A
{
    public int x;
    public void Foo()
    {
        this = new A();
    }
}

Někomu to možná přijde zvláštní, ale opravdu to jde. Hodnotové typy mají jednoduché spravování v paměti, proto pro ně není takový problém měnit svoje data a samy sebe. Ještě, že tohle nejde použít ve třídě. Ono to tak trochu jde, ale ne tímhle způsobem.

Skutečné rozhraní

Představte si, že máme toto rozhraní:

interface IFoo
{
    string Message {get;}
}

Představujete si? Dobrá, teď toto rozhraní instancujeme: IFoo obj = new IFoo("abc");. Namítáte, že to přece nejde, rozhraní a abstraktní třídy nejdou vytvořit. Nejdou, to je pravda. S tím, co jsem vám doposud řekl, tento kód zkompilovat nepůjde, ale takto už ano:

[ComImport, CoClass(typeof(Foo))]
[Guid("00000000-0000-0000-0000-000000000000")]
interface IFoo
{
    string Message {get;}
}

class Foo : IFoo
{
    readonly string name;
    public Foo(string name)
    {
        this.name = name;
    }
    string IFoo.Message
    {
        get{
            return "Ahoj, ja jsem "+name;
        }
    }
}

Atribut CoClass určí třídu, která bude fungovat jako hlavní třída pro toto rozhraní. Nefunguje bez atributu ComImport, který potřebuje ještě atribut Guid. Klidně můžete zadat jakékoliv UUID, ve výsledku se to chová stejně. Teď se místo našeho kódu zkompiluje: IFoo obj = new Foo("abc");

Rekurzivní omezení typového parametru

Co je na tomto kódu špatné?

public class Class<T> where T : Class<T>
{

}

Třída požaduje, aby T bylo typu Class<T>. Těžko se pro toto hledá využití, ale přesto existuje:

public abstract class Class<T> where T : Class<T>
{
    public Class()
    {
        if(!(this is T))
        {
            throw new TypeLoadException();
        }
    }
    
    public abstract T GetMe();
}

public class RealClass : Class<RealClass>
{
    public override RealClass GetMe()
    {
        return new RealClass();
    }
}

public class AnotherClass : Class<RealClass>
{
    public override RealClass GetMe()
    {
        return null;
    }
}

Toto je poměrně hezké využití. Třída Class vyžaduje, aby metoda GetMe vracela instanci podřazené třídy, ale ne samotné třídy Class. Generická omezení sice platí i pro AnotherClass, ale pokud se ji pokusíme vytvořit, selže run-time kontrola a vyhodí vyjímku.

Vypiš číslo, delegáte

Tato zajímavost se zaměřuje na anonymní metody:

using System;
using System.Collections.Generic;


class Test
{
    delegate void Printer();
    
    static void Main()
    {
        List<Printer> printers = new List<Printer>();
        for (int i=0; i < 10; i++)
        {
            printers.Add(delegate { Console.WriteLine(i); });
        }
        
        foreach (Printer printer in printers)
        {
            printer();
        }
        Console.ReadKey(true);
    }
}

Co by ten kód měl dělat? Vypsat čísla od nuly do deseti? Neměl, měl by vypsat čísla od nuly do devíti (to < 10). To ale neudělá. Vtip spočívá ve vytvoření anonymní metody a použití lokální proměnné. Anonymní metoda se kompiluje jako každá jiná a její definice existuje jen jednou v pomocné třídě stvořené kompilátorem. Protože se používá proměnná i v rámci anonymní metody, přestává být lokání proměnnou a stává se polem, aby k ní mohla anonymní metoda přistupovat. Cyklus sice proběhne desetkrát, ale pole i po skončení cyklu zůstane napořád na desítce (Neměla by to být devítka? Ikdyž tam je < 10, i++ se vykoná ještě dříve, poté se vykoná podmínka.). Pak se zavolá desetkrát naše metoda a desetrát se vypíše číslo 10.

Na tento problém si dávejte pozor. Rámec anonymní metody si sám o sobě nepamatuje hodnoty, které byly v místě, kde se metoda v kódu vytvořila. Pamatuje si jen reference na ně, které jsou uložené v pomocné třídě. Pomocná třída se vytvoří ještě předtím, než začne cyklus, proto se v ní bude hodnota měnit s každým dalším opakováním. Řešení je prosté, docílit toho, aby se pomocná třída vytvořila až při každém opakování:

using System;
using System.Collections.Generic;


class Test
{
    delegate void Printer();
    
    static void Main()
    {
        List<Printer> printers = new List<Printer>();
        for (int i=0; i < 10; i++)
        {
            int copy = i;
            printers.Add(delegate { Console.WriteLine(copy); });
        }
        
        foreach (Printer printer in printers)
        {
            printer();
        }
        Console.ReadKey(true);
    }
}

S každou další kopií se vytvoří i nová pomocná třída, obsahující novou hodnotu. Pokud měníte v cyklu proměnnou vytvořenou mimo cyklus, kterou používáte v anonymní metodě definované v kódu uvnitř cyklu, při pozdějším zavolání anonymní metody to prostě získá až poslední hodnotu, která byla nastavena. Pamatujte si vytvářet proměnné s kopií.

Proč 1 není 1

Na tento problém jsem také už párkrát narazil, dá se shrnout tímto kódem:

double d1 = 1.000001;
double d2 = 0.000001;

Console.WriteLine((d1-d2)==1.0);

Vypíše to False. Takto se program chová, protože vyjádření čísel s plovoucí desetinnou čárkou v paměti je o hodně odlišné, než vyjádření celých čísel. Ačkoliv 1.0 jde vyjádřit přesně, 1.000001 je ve skutečnosti 1.0000009999999999177333620536956004798412322998046875 a 0.000001 je 0.000000999999999999999954748111825886258685613938723690807819366455078125. Jejich rozdíl se velmi blíží jedné a proto i metoda ToString() vrací zaokrouhlenou hodnotu 1. Stalo se mi to, když jsem dělal svojí strukturu pro reprezentování komplexních čísel. Měla jednu metodu, která spočítala odmocninu. Když jsem pro kontrolu vynásobil dvě odmocniny z i, vypsalo mi to 1i, ikdyž v metodě ToString jsem kontroloval, jestli je imaginární člen roven 1, potom aby metoda vypsala jen i. Nakonec jsem přišel na to, že to číslo se prostě tak blíží 1, že se vypíše jenom 1, ale porovnávání vrací False. Jde to vyřešit zaokrouhlením Math.Round(d1-d2, 15) pro typ double, nebo Math.Round(d1-d2, 28) pro typ decimal.

—————

Zpět


Diskusní téma: Zvláštnosti jazyka C#

Nebyly nalezeny žádné příspěvky.