Jaka jest różnica między rzucaniem a wymuszaniem?

86

Widziałem, jak oba terminy były używane prawie zamiennie w różnych objaśnieniach online, a większość podręczników, z którymi się zapoznałem, również nie jest do końca jasna co do tego rozróżnienia.

Czy jest może jasny i prosty sposób wyjaśnienia różnicy, którą znacie?

Konwersja typu (czasami znana również jako rzutowanie typu )

Aby użyć wartości jednego typu w kontekście, który oczekuje innego.

Rzucanie typu nonconverting (czasami nazywane kalamburem )

Zmiana, która nie zmienia podstawowych bitów.

Przymus

Proces, za pomocą którego kompilator automatycznie konwertuje wartość jednego typu na wartość innego typu, gdy ten drugi typ jest wymagany przez otaczający kontekst.

Alexandr Kurilin
źródło
4
A co z artykułem w Wikipedii ?
Oliver Charlesworth

Odpowiedzi:

115

Konwersja typu :

Konwersja słów odnosi się do niejawnej lub jawnej zmiany wartości z jednego typu danych na inny, np. 16-bitową liczbę całkowitą na 32-bitową liczbę całkowitą.

Słowo wymuszenie jest używane do określenia niejawnej konwersji.

Rzutowanie słów zazwyczaj odnosi się do jawnej konwersji typu (w przeciwieństwie do niejawnej konwersji), niezależnie od tego, czy jest to ponowna interpretacja wzorca bitowego, czy rzeczywista konwersja.

Zatem przymus jest niejawny, rzutowanie jest jawne, a konwersja jest dowolną z nich.


Kilka przykładów (z tego samego źródła ):

Przymus (niejawny):

double  d;
int     i;
if (d > i)      d = i;

Obsada (jawna):

double da = 3.3;
double db = 3.3;
double dc = 3.4;
int result = (int)da + (int)db + (int)dc; //result == 9
Igor Oks
źródło
czy to spowodowałoby, że „ukryty przymus” byłby zbędny? notatka tutaj używa zarówno „ukrytego przymusu”, jak i „jawnego przymusu”
Dave Cousineau,
1
Niejawną konwersję można przeprowadzić tylko wtedy, gdy nie tracisz precyzji lub nie ma to sensu (np .: Int -> double). W większości współczesnych języków nie możesz zrobić double-> int, ponieważ stracisz precyzję. Z przymusem typu nie stanowi to „problemu”.
Maxime Rouiller,
Ta odpowiedź nie jest zgodna ze specyfikacjami określonymi w ecma 335 dla CIL. Podałem definicję specyfikacji wraz z przykładami w mojej odpowiedzi.
P.Brian. Mackey
24

Jak zauważyłeś, zastosowania są różne.

Moje osobiste zwyczaje to:

  • „Rzutowanie” to użycie operatora rzutowania . Operator rzutowania instruuje kompilator, że (1) nie jest znane to wyrażenie podanego typu, ale obiecuję, że wartość będzie tego typu w czasie wykonywania; kompilator ma traktować wyrażenie jako należące do danego typu, a środowisko wykonawcze zwróci błąd, jeśli tak nie jest, lub (2) wyrażenie jest całkowicie innego typu, ale istnieje dobrze znany sposób kojarzenia instancji typu wyrażenia z wystąpieniami typu rzutowanego. Kompilator ma wygenerować kod, który wykonuje konwersję. Uważny czytelnik zauważy, że są to przeciwieństwa, co moim zdaniem jest zgrabną sztuczką.

  • „Konwersja” to operacja, w wyniku której wartość jednego typu jest traktowana jako wartość innego typu - zwykle innego typu, chociaż technicznie rzecz biorąc „konwersja tożsamości” jest nadal konwersją. Konwersją może być „zmiana reprezentacji”, np. Int na double, lub „zachowanie reprezentacji”, jak ciąg na obiekt. Konwersje mogą być „niejawne”, które nie wymagają rzutowania, lub „jawne”, które wymagają rzutowania.

  • „Przymus” to niejawna konwersja zmieniająca reprezentację.

Eric Lippert
źródło
1
Myślę, że pierwsze zdanie tej odpowiedzi jest najważniejsze. Poszczególne języki używają tych terminów do oznaczenia zupełnie różnych rzeczy. Na przykład u Haskella „przymus” nigdy nie zmienia reprezentacji; bezpieczny przymus, Data.Coerce.coerce :: Coercible a b => a -> bdziała dla typów, które mają taką samą reprezentację; Unsafe.Coerce.unsafeCoerce :: a -> bdziała na dowolne dwa typy (i sprawi, że demony wyjdą z twojego nosa, jeśli użyjesz go źle).
dfeuer
@dfeuer ciekawy punkt danych, dzięki! Zauważam, że specyfikacja C # nie definiuje „przymusu”; moja sugestia jest właśnie tym, co osobiście mam na myśli. Biorąc pod uwagę, że termin wydaje się słabo zdefiniowany, generalnie go unikam.
Eric Lippert
8

Rzutowanie to proces, w którym traktujesz typ obiektu jako inny typ. Wymuszanie polega na konwersji jednego obiektu na inny.

Zauważ, że w poprzednim procesie nie ma konwersji, masz typ, który chciałbyś traktować jako inny, na przykład masz 3 różne obiekty dziedziczące po typie podstawowym i masz metodę, która to przejmie typ podstawowy, w dowolnym momencie, jeśli znasz konkretny typ potomny, możesz go CASTOWAĆ do tego, czym jest i użyć wszystkich określonych metod i właściwości tego obiektu, a to nie utworzy nowej instancji obiektu.

Z drugiej strony, wymuszanie implikuje utworzenie w pamięci nowego obiektu nowego typu, a następnie oryginalny typ zostałby skopiowany do nowego, pozostawiając oba obiekty w pamięci (do momentu, gdy Garbage Collectors zabierze jeden lub oba) .

Jako przykład rozważ następujący kod:

class baseClass {}
class childClass : baseClass {}
class otherClass {}

public void doSomethingWithBase(baseClass item) {}

public void mainMethod()
{
    var obj1 = new baseClass();
    var obj2 = new childClass();
    var obj3 = new otherClass();

    doSomethingWithBase(obj1); //not a problem, obj1 is already of type baseClass
    doSomethingWithBase(obj2); //not a problem, obj2 is implicitly casted to baseClass
    doSomethingWithBase(obj3); //won't compile without additional code
}
  • obj1 jest przekazywany bez rzutowania lub wymuszania (konwersji), ponieważ jest już tego samego typu baseClass
  • obiekt2 jest niejawnie rzutowany do bazy, co oznacza, że ​​nie ma tworzenia nowego obiektu, ponieważ obiekt2 może już być baseClass
  • obj3 musi zostać w jakiś sposób przekonwertowany na base, musisz zapewnić własną metodę konwersji z otherClassdo baseClass, która będzie obejmować utworzenie nowego obiektu typu baseClass i wypełnienie go przez skopiowanie danych z obj3.

Dobrym przykładem jest klasa Convert C #, w której udostępnia niestandardowy kod do konwersji między różnymi typami.

PedroC88
źródło
3
Przykład pomoże wyjaśnić rozróżnienie, które próbujesz wprowadzić.
Oliver Charlesworth
2

Rzutowanie zachowuje typ obiektów. Przymus nie.

Wymuszenie przyjmuje wartość typu, który NIE jest zgodny z przypisaniem i konwertuje na typ, który jest zgodny z przypisaniem. Tutaj wykonuję wymuszenie, ponieważ Int32NIE dziedziczy z Int64... więc NIE jest kompatybilny z przypisaniem. To jest coraz większy przymus (brak utraty danych). Rozszerzający się przymus to niejawna konwersja . Przymus dokonuje konwersji.

void Main()
{
    System.Int32 a = 100;
    System.Int64 b = a;
    b.GetType();//The type is System.Int64.  
}

Rzutowanie pozwala traktować typ tak, jakby był innego typu, jednocześnie zachowując typ .

    void Main()
    {
        Derived d = new Derived();
        Base bb = d;
        //b.N();//INVALID.  Calls to the type Derived are not possible because bb is of type Base
        bb.GetType();//The type is Derived.  bb is still of type Derived despite not being able to call members of Test
    }

    class Base 
    {
        public void M() {}
    }

    class Derived: Base
    {
        public void N() {}
    }

Źródło: The Common Language Infrastructure Annotated Standard Jamesa S. Millera

Co dziwne, dokumentacja Microsoftu dotycząca Casting nie jest zgodna z definicją Casting w specyfikacji ecma-335.

Jawne konwersje (rzutowanie): Jawne konwersje wymagają operatora rzutowania. Przesyłanie jest wymagane, gdy informacje mogą zostać utracone podczas konwersji lub gdy konwersja może się nie powieść z innych powodów. Typowe przykłady obejmują konwersję liczbową na typ, który ma mniejszą precyzję lub mniejszy zakres, oraz konwersję wystąpienia klasy bazowej na klasę pochodną.

... To brzmi jak przymus, a nie rzucanie.

Na przykład,

  object o = 1;
  int i = (int)o;//Explicit conversions require a cast operator
  i.GetType();//The type has been explicitly converted to System.Int32.  Object type is not preserved.  This meets the definition of Coercion not casting.

Kto wie? Może Microsoft sprawdza, czy ktoś to czyta.

P. Brian Mackey
źródło
1

Poniżej znajduje się post z następującego artykułu :

Różnica między przymusem a rzucaniem jest często pomijana. Rozumiem dlaczego; wiele języków ma tę samą (lub podobną) składnię i terminologię dla obu operacji. Niektóre języki mogą nawet odnosić się do dowolnej konwersji jako „rzutowanie”, ale poniższe wyjaśnienie odnosi się do pojęć w CTS.

Jeśli próbujesz przypisać wartość pewnego typu do lokalizacji innego typu, możesz wygenerować wartość nowego typu, która ma podobne znaczenie do oryginału. To jest przymus. Wymuszenie pozwala na użycie nowego typu poprzez utworzenie nowej wartości, która w pewien sposób przypomina oryginał. Niektóre wymuszenia mogą odrzucać dane (np. Konwertowanie int 0x12345678 na krótkie 0x5678), podczas gdy inne nie (np. Konwertowanie int 0x00000008 na krótkie 0x0008 lub długie 0x0000000000000008).

Przypomnij sobie, że wartości mogą mieć wiele typów. Jeśli Twoja sytuacja jest nieco inna i chcesz wybrać tylko inny typ wartości, narzędziem do tego zadania jest rzutowanie. Rzutowanie po prostu wskazuje, że chcesz operować na określonym typie, który zawiera wartość.

Różnica na poziomie kodu różni się od C # do IL. W C # zarówno rzutowanie, jak i przymus wyglądają dość podobnie:

static void ChangeTypes(int number, System.IO.Stream stream)
{
    long longNumber = number;
    short shortNumber = (short)number;

    IDisposable disposableStream = stream;
    System.IO.FileStream fileStream = (System.IO.FileStream)stream;
}

Na poziomie IL są zupełnie inne:

ldarg.0
 conv.i8
 stloc.0

ldarg.0
 conv.i2
 stloc.1


ldarg.1
 stloc.2

ldarg.1
 castclass [mscorlib]System.IO.FileStream
 stloc.3

Jeśli chodzi o poziom logiczny, istnieje kilka ważnych różnic. Najważniejsze do zapamiętania jest to, że przymus tworzy nową wartość, podczas gdy rzucanie nie. Tożsamość wartości pierwotnej i wartości po rzutowaniu są takie same, podczas gdy tożsamość wartości wymuszonej różni się od wartości pierwotnej; koersion tworzy nową, odrębną instancję, podczas gdy rzutowanie nie. Konsekwencją jest to, że wynik rzutowania i oryginał zawsze będą równoważne (zarówno pod względem tożsamości, jak i równości), ale wymuszona wartość może być równa oryginałowi lub nie, i nigdy nie ma takiej samej tożsamości.

W powyższych przykładach łatwo jest zobaczyć konsekwencje przymusu, ponieważ typy liczbowe są zawsze kopiowane według wartości. Podczas pracy z typami referencyjnymi sprawy stają się nieco trudniejsze.

class Name : Tuple<string, string>
{
    public Name(string first, string last)
        : base(first, last)
    {
    }

    public static implicit operator string[](Name name)
    {
        return new string[] { name.Item1, name.Item2 };
    }
}

W poniższym przykładzie jedna konwersja to rzut, a druga to przymus.

Tuple<string, string> tuple = name;
string[] strings = name;

Po tych konwersjach krotka i nazwa są równe, ale ciągi znaków nie są równe żadnemu z nich. Możesz nieco poprawić sytuację (lub nieco bardziej zagmatwać), implementując Equals () i operator == () w klasie Name w celu porównania nazwy i ciągu []. Te operatory „rozwiązałyby” problem z porównaniem, ale nadal istniałyby dwie oddzielne instancje; jakakolwiek modyfikacja łańcuchów nie byłaby odzwierciedlona w nazwie lub krotce, podczas gdy zmiany w nazwie lub krotce byłyby odzwierciedlone w nazwie i krotce, ale nie w łańcuchach.

Chociaż powyższy przykład miał na celu zilustrowanie pewnych różnic między rzutowaniem a wymuszeniem, służy on również jako doskonały przykład tego, dlaczego należy zachować szczególną ostrożność podczas używania operatorów konwersji z typami referencyjnymi w języku C #.

Więzień ZERO
źródło
1

Ze standardu CLI :

I.8.3.2 Przymus

Czasami pożądane jest pobranie wartości typu, którego nie można przypisać - do lokalizacji, i przekonwertowanie wartości na typ, który można przypisać - na typ lokalizacji. Osiąga się to poprzez wymuszenie wartości. Wymuszenie przyjmuje wartość określonego typu i żądanego typu i próbuje utworzyć wartość żądanego typu, która ma znaczenie równoważne z wartością pierwotną. Przymus może skutkować zmianą reprezentacji, jak również zmianą typu; stąd przymus niekoniecznie chroni tożsamość obiektu.

Istnieją dwa rodzaje przymusu: poszerzenie , które nigdy nie powoduje utraty informacji, oraz zawężenie , w którym informacje mogą zostać utracone. Przykładem rozszerzającego się przymusu może być przekształcenie wartości będącej 32-bitową liczbą całkowitą ze znakiem na wartość, która jest 64-bitową liczbą całkowitą ze znakiem. Przykładem przymusu zawężającego jest odwrotność: przekształcenie 64-bitowej liczby całkowitej ze znakiem na 32-bitową liczbę całkowitą ze znakiem. Języki programowania często implementują wymuszenie rozszerzające jako niejawne konwersje , podczas gdy koercje zawężające zwykle wymagają jawnej konwersji .

Pewien wymuszenie jest wbudowane bezpośrednio w operacje VES na typach wbudowanych (patrz §I.12.1). Wszelkie inne formy przymusu powinny być wyraźnie wymagane. W przypadku typów wbudowanych CTS zapewnia operacje do wykonywania rozszerzających koercji bez sprawdzania w czasie wykonywania i zawężania koercji ze sprawdzaniem w czasie wykonywania lub obcięciem, zgodnie z semantyką operacji.

I.8.3.3 Odlewanie

Ponieważ wartość może być więcej niż jednego typu, użycie wartości musi jasno określić, który z jej typów jest używany. Ponieważ wartości są odczytywane z wpisanych lokalizacji, typ używanej wartości jest typem lokalizacji, z której została odczytana. Jeśli ma być używany inny typ, wartość jest rzutowana na jeden z pozostałych typów. Rzutowanie jest zwykle operacją kompilacji, ale jeśli kompilator nie może statycznie wiedzieć, że wartość jest typu docelowego, wykonywane jest sprawdzanie rzutowania w czasie wykonywania. W przeciwieństwie do przymusu, rzutowanie nigdy nie zmienia faktycznego typu obiektu ani nie zmienia reprezentacji. Odlewanie zachowuje tożsamość obiektów.

Na przykład sprawdzenie w czasie wykonywania może być potrzebne podczas rzutowania wartości odczytanej z lokalizacji, która jest wpisana jako przechowująca wartość określonego interfejsu. Ponieważ interfejs jest niepełnym opisem wartości, rzutowanie tej wartości na inny typ interfejsu zwykle powoduje sprawdzenie rzutowania w czasie wykonywania.

naglas
źródło
1

Według Wikipedii

W informatyce konwersja typów, rzutowanie typów, koercja typów i żonglowanie typami to różne sposoby zmiany wyrażenia z jednego typu danych na inny.

Różnica między rzutowaniem typów a koercją typów jest następująca:

           TYPE CASTING           |                   TYPE COERCION
                                  |
1. Explicit i.e., done by user    | 1. Implicit i.e., done by the compiler
                                  |
2. Types:                         | 2. Type:
    Static (done at compile time) |     Widening (conversion to higher data 
                                  |     type)
    Dynamic (done at run time)    |     Narrowing (conversion to lower data 
                                  |     type)
                                  |
3. Casting never changes the      | 3. Coercion can result in representation 
   the actual type of object      |    as well as type change.
   nor representation.            |

Uwaga : przesyłanie nie jest konwersją. To tylko proces, w którym traktujemy typ obiektu jako inny typ. Dlatego rzeczywisty typ obiektu, jak również reprezentacja, nie są zmieniane podczas rzutowania.

Zgadzam się ze słowami @ PedroC88:

Z drugiej strony, wymuszanie implikuje utworzenie w pamięci nowego obiektu nowego typu, a następnie oryginalny typ zostałby skopiowany do nowego, pozostawiając oba obiekty w pamięci (do momentu, gdy Garbage Collectors zabierze jeden lub oba) .

Palak Jain
źródło