Un "blocco downcast se" è una caratteristica linguistica ragionevole?

6

Considera quanto segue "se il cast":

class A     { public void f1() { ... } }
class B : A { public void f2() { ... } }

A a = foo(); // might return A or B

if ( a is B ) {
    //  Inside block, implicitly "redeclare" a as type B
    //  It's B, go for it
    a.f2();
    //  Wouldn't compile.
    a = new A();
}

Non entriamo nel blocco a meno che a sia un B , quindi all'interno del blocco, il compilatore considera a esattamente come se fosse stato dichiarato come tipo B . È possibile assegnare null ad esso nel blocco se si tratta di un tipo di riferimento, ma sarebbe un errore in fase di compilazione per assegnare new A() ad esso, ad esempio (grazie @ThomasEding per aver sollevato tale domanda).

Tutto ciò che potrebbe interrompere quell'ipotesi che a sia B , potrebbe altrettanto facilmente interrompere il seguente C # legale:

if ( a is B ) {
    B b = a as B;

    b.f2();
}

Il primo mi sembra lo zucchero sintattico per quest'ultimo.

Anche se questa funzionalità ha senso, sono sicuro che i ragazzi di Anders Hjelberg hanno caratteristiche più utili da implementare. Mi sto solo chiedendo se è un'idea peggio di quanto io pensi sia.

UPDATE Un'obiezione seria sarebbe che la funzione come descritta aggiunge semantica senza aggiungere sintassi: da una versione della lingua alla successiva, il significato del blocco precedente cambierebbe in modo significativo. Sarebbe più tollerabile con una nuova parola chiave. Ma poi dovresti aggiungere una nuova parola chiave.

Aggiornamento

Una versione più riduttiva di questa funzione è stata implementata in C # 7:

object object2 = new List<Test>();

if (object2 is Dictionary<String, int> foo)
{
    Console.WriteLine(foo.Keys);
}
    
posta Ed Plunkett 18.04.2014 - 22:21
fonte

9 risposte

9

Supponiamo che le caratteristiche inizino con 0 punti, anziché con -100 punti (a causa dei costi associati all'implementazione, alla progettazione, ecc.).

Questa funzione mi sembra un po 'confusa. Ho già dichiarato che a è un A. Se voglio conoscere il tipo a tempo di compilazione di a, devo solo guardare in un posto, alla dichiarazione. Mentre riconosco che l'implementazione è relativamente semplice (usa la sostituzione del codice che descrivi nella tua domanda), anche questa argomenta a favore di non implementare la funzionalità; è facile anche per i programmatori farlo.

La cosa veramente orribile è che il tipo di una variabile può cambiare magicamente subito dopo un } . C # impedisce deliberatamente che ciò accada (vedi I nomi semplici non sono così semplici ). Quindi, odio assolutamente qualsiasi versione di questa funzione che non riesce a introdurre una nuova variabile durante il cast.

Se ti ritrovi costantemente a trasmettere i tuoi tipi in modo da accedere ai membri delle classi derivate, mi viene il sospetto che tu stia abusando dell'ereditarietà. Personalmente, di solito in codice dove sto usando un simile condizionale, uscirò dalla mia funzione se il cast fallisce. Quindi, il mio codice sarebbe più simile a questo:

B myB = myA as B;
if (myB == null) return;

Quale sarebbe, sotto qualche variante della tua proposta, questo:

if (!(myA is B)) return;

o (introducendo un nuovo nome)

if (!(myA to B myB)) return;

o, utilizzando le espressioni dichiarative (questa è una variante su < a href="https://softwareengineering.stackexchange.com/a/236335/15162"> La risposta di Jimmy Hoffa )

if (!a.To(out B myB)) return;

Ad eccezione dell'approccio che utilizza espressioni dichiarative, tutte queste funzioni richiedono l'apprendimento della nuova sintassi C # per una funzionalità che non è affatto dolorosa da gestire. L'approccio dell'espressione dichiarativa è bello in quanto è il caso generale di una caratteristica specifica ; cioè permettendo ai programmatori di utilizzare un compito e condizionali usando una singola espressione.

L'unico costo per l'approccio dell'espressione dichiarativa è che non funzionerà universalmente; funziona solo se il tuo speciale metodo To è implementato e visibile nel tuo ambito corrente. Tuttavia, è un prezzo che sono disposto a pagare per evitare di aggiungere ancora più sintassi al linguaggio C #; Non mi preoccuperò di usare un simile metodo di estensione a meno che non mi occupi di codice che lo richiede costantemente (e probabilmente neanche allora).

In breve: questa funzione non è necessaria abbastanza spesso da giustificare l'aggiunta di nuova sintassi alla lingua. Tuttavia, la caratteristica più generale, le espressioni dichiarative, può valere il costo (motivo per cui Microsoft probabilmente lo pagherà). Non solo gestisce questo caso, ma gestisce anche il fastidioso caso di due dichiarazioni TryParse (e, come la tua proposta, rende l'ambito pulito, il tipo è solo nell'ambito dell'ambito del condizionale).

Aggiornamento: Le espressioni dichiarative sono state cancellate .

Aggiornamento 2: C # 7.0 sta aggiungendo "out variables" . Questo potrebbe essere sufficiente per il mio ultimo esempio ( if (!a.To(out B myB)) return; ). C # 7.0 aggiunge anche "Is-espressioni con pattern" , che consente, if (!(o is int i)) return; . Questa è fondamentalmente la funzione che hai richiesto.

    
risposta data 19.04.2014 - 00:27
fonte
5

Come punto dati, la lingua Ceylon consente a 1 di farlo.

A Ceylon, scriverebbe:

A a = ...
if (is B a) {
   // refer to 'a' as a 'B'
}

o

if (is B b = some_expression_returning_A ) {
   // refer to 'b' as a 'B'
}

C'è anche una variazione dell'istruzione switch che ti permette di attivare i tipi, più o meno allo stesso modo. Ad esempio:

void switchOnEnumTypes(Foo|Bar|Baz var) {
    switch(var)
    case (is Foo) {
        print("FOO");
    }
    case (is Bar) {
        print("BAR");
    }
    case (is Baz) {
        print("BAZ");
    }
}

1 - In realtà, richiede questo, dal momento che Ceylon non ha un cast di tipo esplicito. L'obiettivo è evitare i costrutti del linguaggio di base che possono generare eccezioni. Fanno qualcosa di simile alla progettazione di NPE.

    
risposta data 19.04.2014 - 09:29
fonte
3

Non è una cattiva idea, è solo che non c'è motivo di elevare questo al punto di funzionalità del linguaggio, nemmeno lo zucchero sintattico, puoi facilmente implementarlo tu stesso in C #:

public static class ExtensionMethods
{
    public static void AsIf<T>(this object target, Action<T> todo)
    {
        if (target is T) todo((T)target);
    }
}

a.AsIf(aAsB => {your code});
    
risposta data 18.04.2014 - 22:35
fonte
3

tl; dr: Problemi di concorrenza

Quando scrivi A a; stai effettivamente rendendo l'istruzione " a è una variabile che può contenere oggetti rappresentabili dal tipo A ". Modificando il tipo di variabile in qualcosa di più basso nella catena alimentare, potresti potenzialmente introdurre problemi di concorrenza.

Ad esempio

namespace AsIf
{
class A { public void FA() {} }
class B : A { public void FB() {} }
class C : A { public void FC() {} }

class Program
{
delegate void ActionOnA(ref A a);
static void AToC(ref A a) { Thread.Sleep(5000); a = new C(); }

void main()
{
A a = new B();

// Wait 5 seconds, change a to new C()
ActionOnA action = AToC;
action.BeginInvoke(ref a, (ar) => { action.EndInvoke(ref a, ar); }, null);

if(a is B)
{
// a is implicitly retyped as B for the rest of this block

// B.FB get called on a (an instance of B)
a.FB();
// Sleep 6 seconds
Thread.Sleep(6000);
// B.FB gets called on a (an instance of C)
a.FB();
// 'Undefined behaviour' most likely causing all kinds of crazy
}
}
}
}

È leggermente meno probabile che si verifichi quando si introduce b

// assume everything else is as before

void main()
{
A a = new B();

// Wait 5 seconds, change a to new C()
ActionOnA action = AToC;
action.BeginInvoke(ref a, (ar) => { action.EndInvoke(ref a, ar); }, null);

if(a is B)
{
// introduce b
B b = (B)a;

// B.FB get called on b (an instance of B)
b.FB();
// Sleep 6 seconds
Thread.Sleep(6000);
// B.FB gets called on b (an instance of B)
b.FB();
// Safe! (for now)
}
}

Questo perché B è una variabile completamente diversa che viene utilizzata per contenere lo stesso oggetto. Ciò che la funzione concorrente sta effettivamente facendo è assegnando alla variabile una che non influisce direttamente sull'oggetto in cui si trovava. Poiché b ora contiene un riferimento all'oggetto che era in a , a può essere riassegnato e a nessuno interesserebbe.

Ovviamente questo è un dettaglio di implementazione. Se il compilatore dovesse creare segretamente una seconda variabile per il blocco asif anziché semplicemente reinterpretare a come del tipo B , il risultato sarebbe effettivamente lo stesso del mio secondo esempio. Se un tale comportamento è previsto, ovvio o desiderato è completamente diverso.

Non è una caratteristica impossibile, è solo una che ha più lavoro di quanto valga davvero.

    
risposta data 19.04.2014 - 08:22
fonte
3

Questo può essere fatto con le nuove espressioni-is in C # 7

class A     { public void f1() { ... } }
class B : A { public void f2() { ... } }

A a = foo(); // might return A or B

if ( a is B b ) {
    b.f2();
}
    
risposta data 22.09.2016 - 20:13
fonte
1

Mi piace l'idea. Inoltre, non è pericoloso se prendi in considerazione alcuni casi.

Si consideri

if (a is B) {
    a.BMethod(); // OK
    a = new B(); // OK
    a.BMethod(); // OK
    a = null;    // Disallow? (Probably OK)
    a.BMethod(); // Disallow? (Probably OK)
    a = new A(); // Disallow? (Probably bad)
    a.BMethod(); // Disallow?
}

È certamente possibile scrivere la tua estensione per non compilare in questo caso, ma uno vorrebbe messaggi di errore puliti, è necessario definire più restrizioni (ad esempio potresti assegnare un B a a all'interno del if-block), lancia un'eccezione di runtime o qualcos'altro.

Considera il codice multi-threaded usando a e a viene riassegnato all'interno del blocco as-if. Questo sarebbe molto difficile da fare correttamente in una traduzione diretta in C # (è possibile, ma non molto bello *). Questa funzione deve essere eseguita a livello di codice byte, dove è banale da implementare.

(*): Dovresti assegnare ad un temporaneo, quindi assegnare il a originale, quindi assegnare alla variabile generata dal compilatore a_as_b . Ancora, potrei mancare alcuni importanti casi limite.

    
risposta data 18.04.2014 - 23:11
fonte
1

Ho visto un metodo di estensione AsIf implementato come tale:

private static void AsIf<TBase, TChild>(this TBase source, Action<TChild> action) where TChild : TBase
{
    if ((source is TChild) && (action != null))
    {
        action((TChild)source);
    }
}

I vincoli generici assicurano che tu stia tentando di "as-if" qualcosa di gerarchicamente ammissibile.

    
risposta data 19.04.2014 - 00:32
fonte
1

Questa è già una caratteristica comune in molti linguaggi di programmazione. Sono chiamati tipi di somma . Le classi sono un modo scomodo per simulare i tipi di somma nelle lingue che non li possiedono.

I tipi di somma (chiamati anche varianti e unioni ) sono i duali ai tipi di prodotto (comunemente chiamati tuple ). Proprio come i tipi di prodotto ti permettono di attaccare ai tipi insieme per creare un tipo composto che è composto da tipo 1 e tipo 2, i tipi di somma ti permettono di creare un tipo composto che è o tipo 1 o tipo 2.

Ecco come funziona. Diciamo che vogliamo rappresentare gli indirizzi internet. Esistono due modi per indirizzare un computer su Internet: utilizzando il suo indirizzo IP o il suo nome di dominio completo (FQDN). Il FQDN è una stringa leggibile dall'uomo, mentre l'indirizzo IP è un numero intero compreso tra 0 e 2 ^ 32.

Poiché desideriamo funzioni che possano operare su uno di questi schemi, creiamo un tipo di somma

type InternetAddress = IP of int | FQDN of string

Tieni presente che InternetAddress è in realtà solo un alias di tipo per int + string . Gli identificatori IP e FQDN servono come etichette (chiamate anche costruttori o iniezioni ) per ogni "ramo".

Per creare un valore di tipo InternetAddress , noi iniettiamo un valore nel tipo di somma

val my_address: InternetAddress = FQDN "mydomain.com"

Per utilizzare un valore di un tipo di somma, eseguiamo l'analisi del caso. Il tipo di istanza del valore determina quale "ramo" dell'espressione che prendiamo:

case my_address of
     IP ip_address -> *branch 1*
     | FQDN host_name -> *branch 2*

In entrambi i rami, il valore sottostante di my_address si lega alla variabile dichiarata. Quindi nel ramo 1, possiamo usare ip_address che ha tipo int , e nel ramo 2 possiamo usare host_name che ha tipo string .

A mio parere, i tipi di somma sono una caratteristica indispensabile dei linguaggi di programmazione e il fatto che siano così scarsi nelle lingue tradizionali odierne ha causato negli anni una notevole confusione ed errori.

    
risposta data 22.09.2016 - 20:13
fonte
1

In Swift scrivi:

if let b = a as? B {
    ....
}

"a as B" proverebbe a convertire a a B - che per i tipi di oggetto avrebbe successo se un effettivamente appartiene alla classe B che avrebbe bisogno di essere una sottoclasse o superclasse di A per avere successo. Ovviamente questo cambierebbe solo il tipo di riferimento, non il tipo dell'oggetto dietro. E si bloccherebbe di proposito se la conversione fallisse.

"a as? B" è simile tranne che restituisce una "B opzionale"; il risultato è nullo se la conversione fallisce.

"if let b = a as as? B" valuta (a as? B). Se il risultato è zero, il if non viene eseguito. Se il risultato non è zero, assegna il riferimento a un non facoltativo b di tipo B ed esegue if.

    
risposta data 22.09.2016 - 21:08
fonte

Leggi altre domande sui tag