Przesyłanie vs. użycie słowa kluczowego „as” w CLR

386

Podczas programowania interfejsów odkryłem, że dużo rzucam lub przekształcam typy obiektów.

Czy istnieje różnica między tymi dwiema metodami konwersji? Jeśli tak, to czy istnieje różnica kosztów lub jak to wpływa na mój program?

public interface IMyInterface
{
    void AMethod();
}

public class MyClass : IMyInterface
{
    public void AMethod()
    {
       //Do work
    }

    // Other helper methods....
}

public class Implementation
{
    IMyInterface _MyObj;
    MyClass _myCls1;
    MyClass _myCls2;

    public Implementation()
    {
        _MyObj = new MyClass();

        // What is the difference here:
        _myCls1 = (MyClass)_MyObj;
        _myCls2 = (_MyObj as MyClass);
    }
}

Ponadto, jaka jest „ogólnie” preferowana metoda?

Frank V.
źródło
Czy możesz dodać mały przykład, dlaczego używasz castów w pytaniu, a może zacząć nowy? Interesuje mnie, dlaczego potrzebujesz obsady tylko do testów jednostkowych. Myślę jednak, że jest to poza zakresem tego pytania.
Erik van Brakel
2
Prawdopodobnie mogę zmienić test jednostkowy, aby temu zapobiec. Zasadniczo sprowadza się to do tego, że mam właściwość na moim konkretnym obiekcie, której nie ma w interfejsie. Muszę ustawić tę własność, ale w rzeczywistości ta własność zostałaby ustalona w inny sposób. Czy to jest odpowiedź na Twoje pytanie?
Frank V
Jak zauważył poniżej Patrik Hägne, JEST różnica.
Neil

Odpowiedzi:

517

Odpowiedź poniżej linii została napisana w 2008 roku.

W C # 7 wprowadzono dopasowanie wzorca, które w dużej mierze zastąpiło asoperatora, ponieważ można teraz pisać:

if (randomObject is TargetType tt)
{
    // Use tt here
}

Zauważ, że ttpo tym jest nadal w zasięgu, ale nie jest zdecydowanie przypisany. (Jest to zdecydowanie przypisane w ramachif ciele.) W niektórych przypadkach jest to nieco irytujące, więc jeśli naprawdę zależy ci na wprowadzeniu możliwie najmniejszej liczby zmiennych w każdym zakresie, możesz nadal chcieć użyć isrzutowania.


Nie sądzę, aby jakakolwiek z dotychczasowych odpowiedzi (w momencie rozpoczynania tej odpowiedzi!) Naprawdę wyjaśniała, gdzie warto jej użyć.

  • Nie rób tego:

    // Bad code - checks type twice for no reason
    if (randomObject is TargetType)
    {
        TargetType foo = (TargetType) randomObject;
        // Do something with foo
    }

    Nie tylko sprawdza się to dwukrotnie, ale może sprawdzać różne rzeczy, jeśli randomObjectjest to pole, a nie zmienna lokalna. Możliwe jest, że „if” przejdzie, ale rzutowanie zakończy się niepowodzeniem, jeśli inny wątek zmieni wartośćrandomObject między nimi.

  • Jeśli randomObjectnaprawdę powinno to być wystąpienie TargetType, tzn. Jeśli tak nie jest, oznacza to błąd, to casting jest właściwym rozwiązaniem. To natychmiast zgłasza wyjątek, co oznacza, że ​​nie wykonuje się żadnej pracy przy niepoprawnych założeniach, a wyjątek poprawnie pokazuje rodzaj błędu.

    // This will throw an exception if randomObject is non-null and
    // refers to an object of an incompatible type. The cast is
    // the best code if that's the behaviour you want.
    TargetType convertedRandomObject = (TargetType) randomObject;
  • Jeśli randomObject może to być instancja TargetTypei TargetTypejest typem odwołania, użyj kodu w następujący sposób:

    TargetType convertedRandomObject = randomObject as TargetType;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject
    }
  • Jeśli randomObject może być instancją typu TargetTypei TargetTypejest typem wartości, nie możemy użyć go asrazem TargetType, ale możemy użyć typu zerowego:

    TargetType? convertedRandomObject = randomObject as TargetType?;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject.Value
    }

    (Uwaga: obecnie jest to w rzeczywistości wolniejsze niż + cast . Myślę, że jest bardziej eleganckie i spójne, ale proszę bardzo.)

  • Jeśli naprawdę nie potrzebujesz przekonwertowanej wartości, ale musisz tylko wiedzieć, czy jest to instancja TargetType, a następnieis operator jest Twoim przyjacielem. W tym przypadku nie ma znaczenia, czy TargetType jest typem referencyjnym czy typem wartości.

  • Mogą wystąpić inne przypadki dotyczące leków generycznych is są użyteczne (ponieważ możesz nie wiedzieć, czy T jest typem referencyjnym, czy nie, więc nie możesz użyć jako), ale są one stosunkowo niejasne.

  • Prawie na pewno wcześniej użyłem isprzypadku typu wartości, nie myśląc o użyciu typu zerowalnego i asrazem :)


EDYCJA: Zauważ, że żadne z powyższych nie mówi o wydajności, poza przypadkiem typu wartości, w którym zauważyłem, że rozpakowanie do typu wartości zerowej jest w rzeczywistości wolniejsze - ale spójne.

Zgodnie z odpowiedzią naasking, zarówno rzutowanie jak i rzucanie jest zarówno szybkie jak i zerowe przy pomocy nowoczesnych JIT, jak pokazano w poniższym kodzie:

using System;
using System.Diagnostics;
using System.Linq;

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "x";
            values[i + 2] = new object();
        }
        FindLengthWithIsAndCast(values);
        FindLengthWithIsAndAs(values);
        FindLengthWithAsAndNullCheck(values);
    }

    static void FindLengthWithIsAndCast(object[] values)        
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            if (o is string)
            {
                string a = (string) o;
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("Is and Cast: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithIsAndAs(object[] values)        
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            if (o is string)
            {
                string a = o as string;
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("Is and As: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithAsAndNullCheck(object[] values)        
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            string a = o as string;
            if (a != null)
            {
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("As and null check: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }
}

Na moim laptopie wszystkie działają w około 60ms. Dwie rzeczy do zapamiętania:

  • Nie ma między nimi znaczącej różnicy. (W rzeczywistości zdarzają się sytuacje, w których sprawdzenie „plus-zero” jest zdecydowanie wolniejsze. Powyższy kod w rzeczywistości ułatwia sprawdzanie typu, ponieważ dotyczy klasy zapieczętowanej; jeśli sprawdzasz interfejs, saldo lekko się przechyla na korzyść kontroli plus-zerowej).
  • Wszystkie są niesamowicie szybkie. To po prostu nie będzie wąskim gardłem w kodzie, chyba że naprawdę nie zamierzasz nic robić z wartościami później.

Nie martwmy się więc o wydajność. Martwmy się o poprawność i spójność.

Uważam, że zarówno rzutowanie, jak i rzutowanie jest niebezpieczne w przypadku zmiennych, ponieważ rodzaj wartości, do której się odnosi, może się zmienić z powodu innego wątku między testem a rzutowaniem. To byłaby dość rzadka sytuacja - ale wolałbym mieć konwencję, z której mogę konsekwentnie korzystać.

Twierdzę również, że kontrola „jak wtedy-zero” zapewnia lepszy rozdział obaw. Mamy jedno zdanie, które próbuje przekształcić, a następnie jedno zdanie, które wykorzystuje wynik. Is-and-cast lub is-and-as wykonuje test, a następnie kolejną próbę konwersji wartości.

Innymi słowy, czy ktoś kiedykolwiek napisałby:

int value;
if (int.TryParse(text, out value))
{
    value = int.Parse(text);
    // Use value
}

Tak właśnie się dzieje - choć oczywiście w tańszy sposób.

Jon Skeet
źródło
7
Oto koszt / as / castingu w kategoriach IL: atalasoft.com/cs/blogs/stevehawley/archive/2009/01/30/…
cokół
3
W przypadku, gdy obiekt docelowy może być typem docelowym, dlaczego użycie kombinacji „jest” i rzutowania jest uważane za złą praktykę? To znaczy, generuje wolniejszy kod, ale w tym przypadku intencje są bardziej jasne niż rzutowanie AS, takie jak „Zrób coś, jeśli celObject jest celemType”, zamiast „Zrób coś, jeśli celObject ma wartość zerową”. Ponadto klauzula AS utworzy niepotrzebną zmienną poza zakresem IF.
Valera Kolupaev
2
@Valera: Dobre punkty, chociaż sugerowałbym, że test as / null jest wystarczająco idiomatyczny, aby intencja była jasna dla prawie wszystkich programistów C #. Osobiście nie podoba mi się duplikacja związana z obsadą jest +. Właściwie chciałbym coś w rodzaju konstrukcji „jak gdyby”, która wykonuje obie akcje w jednym. Często się spotykają ...
Jon Skeet
2
@Jon Skeet: przepraszam za spóźnienie Iss and Cast: 2135, Is And As: 2145, As And null check: 1961, specs: OS: Windows Seven, CPU: i5-520M, 4GB DDR3 1033 ram, benchmark on array z 128 000 000 pozycji.
Behrooz
2
Z C # 7 możesz zrobić: if (randomObject is TargetType convertedRandomObject){ // Do stuff with convertedRandomObject.Value}lub użyć switch/ case zobaczyć dokumenty
WerWet
71

„as” zwróci NULL, jeśli nie będzie możliwe przesłanie.

rzucanie wcześniej spowoduje wyjątek.

W przypadku wydajności zgłoszenie wyjątku jest zwykle bardziej kosztowne w czasie.

Patrick Desjardins
źródło
3
Zgłaszanie wyjątków jest bardziej kosztowne, ale jeśli wiesz, że obiekt można rzucić poprawnie, ponieważ wymaga to więcej czasu ze względu na kontrolę bezpieczeństwa (patrz odpowiedź Antona). Koszt kontroli bezpieczeństwa jest jednak, jak sądzę, dość niewielki.
17
Należy wziąć pod uwagę koszt potencjalnego zgłoszenia wyjątku, ale często jest to poprawny projekt.
Jeffrey L Whitledge
@ panesofglass - W przypadku typów referencyjnych zgodność konwersji będzie zawsze sprawdzana w czasie wykonywania zarówno dla as, jak i rzutowania, aby czynnik nie rozróżniał dwóch opcji. (Gdyby tak nie było, obsada nie mogłaby podnieść wyjątku.)
Jeffrey L Whitledge
4
@Frank - jeśli musisz na przykład użyć kolekcji pre-generics, a metoda w interfejsie API wymaga listy pracowników, a joker zamiast tego przekazuje listę produktów, może być odpowiedni niepoprawny wyjątek rzutowania naruszenie wymagań interfejsu.
Jeffrey L Whitledge
27

Oto kolejna odpowiedź, z pewnym porównaniem IL. Rozważ klasę:

public class MyClass
{
    public static void Main()
    {
        // Call the 2 methods
    }

    public void DirectCast(Object obj)
    {
        if ( obj is MyClass)
        { 
            MyClass myclass = (MyClass) obj; 
            Console.WriteLine(obj);
        } 
    } 


    public void UsesAs(object obj) 
    { 
        MyClass myclass = obj as MyClass; 
        if (myclass != null) 
        { 
            Console.WriteLine(obj);
        } 
    }
}

Teraz spójrz na IL, którą wytwarza każda metoda. Nawet jeśli kody operacyjne nic dla ciebie nie znaczą, możesz zauważyć jedną zasadniczą różnicę - wywoływana jest isinst, po której następuje castclass w metodzie DirectCast. Więc dwa połączenia zamiast jednego w zasadzie.

.method public hidebysig instance void  DirectCast(object obj) cil managed
{
  // Code size       22 (0x16)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  isinst     MyClass
  IL_0006:  brfalse.s  IL_0015
  IL_0008:  ldarg.1
  IL_0009:  castclass  MyClass
  IL_000e:  pop
  IL_000f:  ldarg.1
  IL_0010:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0015:  ret
} // end of method MyClass::DirectCast

.method public hidebysig instance void  UsesAs(object obj) cil managed
{
  // Code size       17 (0x11)
  .maxstack  1
  .locals init (class MyClass V_0)
  IL_0000:  ldarg.1
  IL_0001:  isinst     MyClass
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  brfalse.s  IL_0010
  IL_000a:  ldarg.1
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ret
} // end of method MyClass::UsesAs

Słowo kluczowe isinst vs. castclass

Ten post na blogu zawiera przyzwoite porównanie dwóch sposobów. Jego streszczenie to:

  • W bezpośrednim porównaniu, isinst jest szybszy niż castclass (choć tylko nieznacznie)
  • Gdy trzeba było sprawdzić, czy konwersja się powiodła, isinst był znacznie szybszy niż klasa Cast Cast
  • Nie należy używać kombinacji isinst i castclass, ponieważ było to znacznie wolniejsze niż najszybsza „bezpieczna” konwersja (ponad 12% wolniej)

Ja osobiście zawsze używam As, ponieważ jest łatwy do odczytania i jest zalecany przez zespół programistów .NET (lub Jeffrey Richter i tak)

Chris S.
źródło
Szukałem jasnego wyjaśnienia dla castingu vs as, ta odpowiedź jest znacznie bardziej przejrzysta, ponieważ obejmuje wspólny język pośredni krok po kroku. Dzięki!
Morse
18

Jedną z bardziej subtelnych różnic między nimi jest to, że słowa kluczowego „as” nie można używać do rzutowania, gdy zaangażowany jest operator rzutowania:

public class Foo
{
    public string Value;

    public static explicit operator string(Foo f)
    {
        return f.Value;
    }

}

public class Example
{
    public void Convert()
    {
        var f = new Foo();
        f.Value = "abc";

        string cast = (string)f;
        string tryCast = f as string;
    }
}

Nie kompiluje się (chociaż myślę, że tak było w poprzednich wersjach) w ostatnim wierszu, ponieważ słowa kluczowe „as” nie uwzględniają operatorów rzutowania. Linia string cast = (string)f;działa jednak dobrze.

Patrik Hägne
źródło
12

as nigdy nie zgłasza wyjątku, jeśli nie może zamiast tego wykonać konwersji zwracając null ( ponieważ działa tylko na typach referencyjnych). Więc użycie as jest w zasadzie równoważne z

_myCls2 = _myObj is MyClass ? (MyClass)_myObj : null;

Natomiast rzutki w stylu C rzucają wyjątek, gdy konwersja nie jest możliwa.

Anton Gogolev
źródło
4
Odpowiednik, tak, ale nie to samo. Generuje to znacznie więcej kodu niż as.
cokół
10

Nie do końca odpowiedź na twoje pytanie, ale to, co uważam za ważny powiązany punkt.

Jeśli programujesz do interfejsu, nie powinno być potrzeby przesyłania. Mam nadzieję, że te obsady są bardzo rzadkie. Jeśli nie, prawdopodobnie musisz ponownie przemyśleć niektóre interfejsy.

ropucha
źródło
Jak dotąd casting był głównie potrzebny do moich testów jednostkowych, ale dziękuję za jego poruszenie. Będę o tym pamiętać, pracując nad tym.
Frank V
Zgadzam się z ropuchą, jestem również ciekawy, dlaczego aspekt testowania jednostkowego jest odpowiedni dla castingu dla ciebie @Frank V.Jeśli istnieje potrzeba rzucenia, często potrzeba przeprojektowania lub refaktoryzacji, ponieważ sugeruje to, że próbujesz rozwiązywać różne problemy, gdzie należy nimi zarządzać inaczej.
Senator
@TheSenator To pytanie ma ponad 3 lata, więc tak naprawdę nie pamiętam. Ale prawdopodobnie agresywnie korzystałem z interfejsów, nawet podczas testów jednostkowych. Być może dlatego, że użyłem wzorca fabrycznego i nie miałem dostępu do publicznego konstruktora obiektów docelowych do przetestowania.
Frank V
9

Proszę zignorować rady Jona Skeeta, a mianowicie: unikać wzorca testowania i rzucania, tj .:

if (randomObject is TargetType)
{
    TargetType foo = randomObject as TargetType;
    // Do something with foo
}

Pomysł, że kosztuje to więcej niż rzut i test zerowy, to MIT :

TargetType convertedRandomObject = randomObject as TargetType;
if (convertedRandomObject != null)
{
    // Do stuff with convertedRandomObject
}

To mikrooptymalizacja, która nie działa. Przeprowadziłem kilka prawdziwych testów , a testowanie i przesyłanie jest w rzeczywistości szybsze niż porównanie rzutowania i zerowania, a także jest bezpieczniejsze, ponieważ nie masz możliwości posiadania pustego odwołania w zakresie poza rzutowaniem zawieść.

Jeśli chcesz mieć powód, dla którego testowanie i przesyłanie jest szybsze lub przynajmniej wolniejsze, istnieje prosty i złożony powód.

Proste: nawet naiwne kompilatory połączą dwie podobne operacje, takie jak testowanie i odlewanie, w jeden test i gałąź. test cast-and-null może wymusić dwa testy i odgałęzienie, jeden dla testu typu i konwersji na zero w przypadku niepowodzenia, jeden dla samego sprawdzenia null. Przynajmniej obie zoptymalizują się do pojedynczego testu i gałęzi, więc testowanie i rzucanie nie byłoby ani wolniejsze, ani szybsze niż rzutowanie i zerowanie.

Złożona: dlaczego testowanie i rzutowanie jest szybsze: testowanie rzutowe i zerowe wprowadza kolejną zmienną do zakresu zewnętrznego, którą kompilator musi śledzić pod kątem żywotności, i może nie być w stanie zoptymalizować tej zmiennej w zależności od stopnia złożoności kontroli przepływ jest. I odwrotnie, test-and-cast wprowadza nową zmienną tylko w ograniczonym zakresie, dzięki czemu kompilator wie, że zmienna jest martwa po wyjściu z zakresu, a zatem może lepiej zoptymalizować alokację rejestru.

PROSZĘ więc, niech ten „test„ rzuć i zeruj ”jest lepszy niż„ testuj i odrzuć ”poradę DIE. PROSZĘ. test-and-cast jest zarówno bezpieczniejszy, jak i szybszy.

naasking
źródło
7
@naasking: Jeśli testujesz dwa razy (jak na pierwszym fragmencie), istnieje szansa, że ​​typ zmieni się między dwoma testami, jeśli jest to pole lub refparametr. Jest bezpieczny dla zmiennych lokalnych, ale nie dla pól. Byłbym zainteresowany uruchomieniem testów, ale kod podany w poście na blogu nie jest kompletny. Zgadzam się z brakiem mikrooptymalizacji, ale nie sądzę, aby dwukrotne użycie tej wartości było bardziej czytelne lub eleganckie niż użycie „as” i testu nieważności. (Zdecydowanie użyłbym prostej obsady, a nie „as” po b, btw.)
Jon Skeet
5
Nie rozumiem też, dlaczego jest to bezpieczniejsze. Pokazałem, dlaczego tak naprawdę jest mniej bezpieczne. Jasne, kończy się zmienna o zasięgu, która może mieć wartość null, ale jeśli nie zaczniesz używać jej poza zakresem kolejnego bloku „jeśli”, nic ci nie jest. Podniesione przeze mnie obawy dotyczące bezpieczeństwa (wokół pól zmieniających swoją wartość) są prawdziwym problemem z przedstawionym kodem - obawy dotyczące bezpieczeństwa wymagają od deweloperów rozluźnienia innego kodu.
Jon Skeet,
1
+1 za wskazanie, że jest / obsada lub as / obsada nie jest w rzeczywistości wolniejsze, pamiętaj. Po samodzielnym przeprowadzeniu pełnego testu mogę potwierdzić, że nie ma znaczenia, o ile widzę - i szczerze mówiąc, możesz przeprowadzić niesamowitą liczbę rzutów w bardzo krótkim czasie. Zaktualizuje moją odpowiedź pełnym kodem.
Jon Skeet
1
Rzeczywiście, jeśli powiązanie nie jest lokalne, istnieje szansa na błąd TOCTTOU (czas od sprawdzenia do czasu użycia), więc warto to zrobić. Jeśli chodzi o to, dlaczego jest bezpieczniej, współpracuję z wieloma młodszymi programistami, którzy z jakiegoś powodu lubią ponownie używać lokalnych użytkowników. cast-and-null jest więc bardzo realnym zagrożeniem z mojego doświadczenia i nigdy nie spotkałem się z sytuacją TOCTTOU, ponieważ nie projektuję mojego kodu w ten sposób. Jeśli chodzi o szybkość testowania środowiska wykonawczego, jest nawet szybsza niż wirtualna wysyłka [1]! Re: kod, zobaczę, czy mogę znaleźć źródło testu rzutowania. [1] upperlogics.blogspot.com/2008/10/…
naasking
1
@naasking: Nigdy nie spotkałem się z problemem lokalnego ponownego użycia - ale powiedziałbym, że łatwiej jest zauważyć w przeglądzie kodu niż bardziej subtelny błąd TOCTTOU. Warto również zauważyć, że właśnie uruchomiłem ponownie własny test porównawczy dla interfejsów zamiast zamkniętej klasy, a to przewyższa wydajność na korzyść sprawdzania zerowego ... ale, jak powiedziałem, wydajność nie jest dlatego wybrałbym tutaj jakieś szczególne podejście.
Jon Skeet
4

Jeśli rzutowanie się nie powiedzie, słowo kluczowe „as” nie zgłasza wyjątku; zamiast tego ustawia zmienną na null (lub na wartość domyślną dla typów wartości).

Smerf
źródło
3
Brak wartości domyślnych dla typów wartości. Ponieważ nie można go używać do rzutowania typów wartości.
Patrik Hägne
2
Słowo kluczowe „as” w rzeczywistości nie działa na typach wartości, więc zawsze ma wartość null.
Erik van Brakel
4

To nie jest odpowiedź na pytanie, ale komentarz do przykładu kodu pytania:

Zwykle nie trzeba rzutować obiektu z np. IMyInterface na MyClass. Wspaniałą rzeczą w interfejsach jest to, że jeśli weźmiesz obiekt jako dane wejściowe, które implementuje interfejs, nie musisz się martwić, jaki rodzaj obiektu otrzymujesz.

Jeśli rzucisz IMyInterface na MyClass, to już zakładasz, że otrzymujesz obiekt typu MyClass i nie ma sensu używać IMyInterface, ponieważ jeśli karmisz swój kod innymi klasami, które implementują IMyInterface, spowoduje to uszkodzenie twojego kodu ...

Teraz moja rada: jeśli twoje interfejsy są dobrze zaprojektowane, możesz uniknąć wielu rzutów.

f3lix
źródło
3

asOperator może być używany tylko w typach referencyjnych, to nie może być przeciążona, a powróci null, jeśli operacja się nie powiedzie. Nigdy nie rzuci wyjątku.

Przesyłanie może być zastosowane na dowolnym kompatybilnym typie, może być przeciążone i spowoduje wyjątek, jeśli operacja się nie powiedzie.

Wybór zastosowania zależy od okoliczności. Przede wszystkim chodzi o to, czy chcesz zgłosić wyjątek w przypadku nieudanej konwersji.

Jeffrey L. Whitledge
źródło
1
„as” może być również użyte w przypadku typów wartości dopuszczających wartości zerowe, co stanowi interesujący wzorzec. Zobacz moją odpowiedź na kod.
Jon Skeet
1

Moja odpowiedź dotyczy tylko prędkości w przypadkach, gdy nie sprawdzamy typu i nie sprawdzamy wartości zerowych po rzuceniu. Dodałem dwa dodatkowe testy do kodu Jona Skeeta:

using System;
using System.Diagnostics;

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];

        for (int i = 0; i < Size; i++)
        {
            values[i] = "x";
        }
        FindLengthWithIsAndCast(values);
        FindLengthWithIsAndAs(values);
        FindLengthWithAsAndNullCheck(values);

        FindLengthWithCast(values);
        FindLengthWithAs(values);

        Console.ReadLine();
    }

    static void FindLengthWithIsAndCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            if (o is string)
            {
                string a = (string)o;
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("Is and Cast: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithIsAndAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            if (o is string)
            {
                string a = o as string;
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("Is and As: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithAsAndNullCheck(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            string a = o as string;
            if (a != null)
            {
                len += a.Length;
            }
        }
        sw.Stop();
        Console.WriteLine("As and null check: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }
    static void FindLengthWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            string a = (string)o;
            len += a.Length;
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            string a = o as string;
            len += a.Length;
        }
        sw.Stop();
        Console.WriteLine("As: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }
}

Wynik:

Is and Cast: 30000000 : 88
Is and As: 30000000 : 93
As and null check: 30000000 : 56
Cast: 30000000 : 66
As: 30000000 : 46

Nie próbuj koncentrować się na szybkości (tak jak ja), ponieważ wszystko to jest bardzo szybkie.

CoperNick
źródło
Podobnie w moich testach stwierdziłem, że askonwersja (bez sprawdzania błędów) przebiegała około 1-3% szybciej niż rzutowanie (około 540 ms wobec 550 ms przy 100 milionach iteracji). Ani nie złoży ani nie złamie twojej aplikacji.
palswim
1

Oprócz wszystkiego, co już tu ujawniłem, właśnie natrafiłem na praktyczną różnicę, którą moim zdaniem warto zauważyć, między jawnym castingiem

var x = (T) ...

w porównaniu z użyciem as operatora.

Oto przykład:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(GenericCaster<string>(12345));
        Console.WriteLine(GenericCaster<object>(new { a = 100, b = "string" }) ?? "null");
        Console.WriteLine(GenericCaster<double>(20.4));

        //prints:
        //12345
        //null
        //20.4

        Console.WriteLine(GenericCaster2<string>(12345));
        Console.WriteLine(GenericCaster2<object>(new { a = 100, b = "string" }) ?? "null");

        //will not compile -> 20.4 does not comply due to the type constraint "T : class"
        //Console.WriteLine(GenericCaster2<double>(20.4));
    }

    static T GenericCaster<T>(object value, T defaultValue = default(T))
    {
        T castedValue;
        try
        {
            castedValue = (T) Convert.ChangeType(value, typeof(T));
        }
        catch (Exception)
        {
            castedValue = defaultValue;
        }

        return castedValue;
    }

    static T GenericCaster2<T>(object value, T defaultValue = default(T)) where T : class
    {
        T castedValue;
        try
        {
            castedValue = Convert.ChangeType(value, typeof(T)) as T;
        }
        catch (Exception)
        {
            castedValue = defaultValue;
        }

        return castedValue;
    }
}

Konkluzja: GenericCaster2 nie będzie działać z typami struktur. GenericCaster będzie.

Veverke
źródło
1

W przypadku korzystania z pakietu Office PIA kierowania .NET Framework 4.X należy użyć jako hasła, w przeciwnym razie nie będzie skompilować.

Microsoft.Office.Interop.Outlook.Application o = new Microsoft.Office.Interop.Outlook.Application();
Microsoft.Office.Interop.Outlook.MailItem m = o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem) as Microsoft.Office.Interop.Outlook.MailItem;

Przesyłanie jest jednak prawidłowe podczas celowania w .NET 2.0:

Microsoft.Office.Interop.Outlook.MailItem m = (Microsoft.Office.Interop.Outlook.MailItem)o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem);

Podczas celowania w .NET 4.X błędy to:

błąd CS0656: Brak wymaganego kompilatora członek „Microsoft.CSharp.RuntimeBinder.Binder.Convert”

błąd CS0656: Brak wymaganego kompilatora element członkowski „Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create”

Olivier MATROT
źródło
0

Słowo askluczowe działa tak samo, jak jawne rzutowanie między zgodnymi typami referencyjnymi, z tą różnicą, że nie powoduje wyjątku w przypadku niepowodzenia konwersji. Daje raczej wartość zerową w zmiennej docelowej. Ponieważ wyjątki są bardzo drogie pod względem wydajności, uważa się je za znacznie lepszą metodę odlewania.

Cerebrus
źródło
Nie to samo, ponieważ jeden wywołuje CastClass, a drugi IsInst w kodzie IL.
Jenix
0

To, co wybierzesz, zależy od tego, co jest wymagane. Wolę jawne castingi

IMyInterface = (IMyInterface)someobj;

ponieważ jeśli obiekt powinien być typu IMyInterface, a nie jest - to zdecydowanie problem. Lepiej jest otrzymać błąd jak najwcześniej, ponieważ dokładny błąd zostanie naprawiony zamiast naprawiać jego efekt uboczny.

Ale jeśli masz do czynienia z metodami, które akceptują objectjako parametr, musisz sprawdzić jego dokładny typ przed wykonaniem jakiegokolwiek kodu. W takim przypadku asprzydatne byłoby uniknięcie InvalidCastException.

Oleg
źródło
0

Zależy, czy po sprawdzeniu „as” chcesz sprawdzić wartość null, czy wolisz, aby aplikacja zgłosiła wyjątek?

Moją ogólną zasadą jest, jeśli zawsze oczekuję, że zmienna będzie typu, którego oczekuję w momencie, gdy chcę użyć rzutowania. Jeśli możliwe jest, że zmienna nie będzie rzutować na to, czego chcę i jestem przygotowany do obsługi wartości zerowych przy użyciu as, użyję jako.

Darryl Braaten
źródło
0

Problem PO jest ograniczony do konkretnej sytuacji rzutowania. Tytuł obejmuje znacznie więcej sytuacji.
Oto przegląd wszystkich istotnych sytuacji castingowych, o których obecnie mogę myśleć:

private class CBase
{
}

private class CInherited : CBase
{
}

private enum EnumTest
{
  zero,
  one,
  two
}

private static void Main (string[] args)
{
  //########## classes ##########
  // object creation, implicit cast to object
  object oBase = new CBase ();
  object oInherited = new CInherited ();

  CBase oBase2 = null;
  CInherited oInherited2 = null;
  bool bCanCast = false;

  // explicit cast using "()"
  oBase2 = (CBase)oBase;    // works
  oBase2 = (CBase)oInherited;    // works
  //oInherited2 = (CInherited)oBase;   System.InvalidCastException
  oInherited2 = (CInherited)oInherited;    // works

  // explicit cast using "as"
  oBase2 = oBase as CBase;
  oBase2 = oInherited as CBase;
  oInherited2 = oBase as CInherited;  // returns null, equals C++/CLI "dynamic_cast"
  oInherited2 = oInherited as CInherited;

  // testing with Type.IsAssignableFrom(), results (of course) equal the results of the cast operations
  bCanCast = typeof (CBase).IsAssignableFrom (oBase.GetType ());    // true
  bCanCast = typeof (CBase).IsAssignableFrom (oInherited.GetType ());    // true
  bCanCast = typeof (CInherited).IsAssignableFrom (oBase.GetType ());    // false
  bCanCast = typeof (CInherited).IsAssignableFrom (oInherited.GetType ());    // true

  //########## value types ##########
  int iValue = 2;
  double dValue = 1.1;
  EnumTest enValue = EnumTest.two;

  // implicit cast, explicit cast using "()"
  int iValue2 = iValue;   // no cast
  double dValue2 = iValue;  // implicit conversion
  EnumTest enValue2 = (EnumTest)iValue;  // conversion by explicit cast. underlying type of EnumTest is int, but explicit cast needed (error CS0266: Cannot implicitly convert type 'int' to 'test01.Program.EnumTest')

  iValue2 = (int)dValue;   // conversion by explicit cast. implicit cast not possible (error CS0266: Cannot implicitly convert type 'double' to 'int')
  dValue2 = dValue;
  enValue2 = (EnumTest)dValue;  // underlying type is int, so "1.1" beomces "1" and then "one"

  iValue2 = (int)enValue;
  dValue2 = (double)enValue;
  enValue2 = enValue;   // no cast

  // explicit cast using "as"
  // iValue2 = iValue as int;   error CS0077: The as operator must be used with a reference type or nullable type
}
Tobias Knauss
źródło