Perché il backing store di un array si lega al tipo più piccolo?

1

In C # è legale scrivere

Animal[] a = new Giraffe[4]; //with obvious relationships

Perché gli array sono covarianti. Tuttavia, questa è una relazione di rottura. È un'eccezione di runtime per applicare quindi un diverso tipo di animale a questo array perché il backing store è un Giraffe .

a[0] = new Cat(); //KABOOM

Perché il linguaggio non crea un backing store covariante? Questo non sembra essere una limitazione del linguaggio perché posso creare personalmente questo tipo di struttura dati:

class Program
{
    static void Main(string[] args)
    {
        A[] a = new B[2];
        a[0] = new B();
        a[1] = new C();//runtime exception

        var containerLegal = new Container<B, A>();
        containerLegal.Add(new B());
        containerLegal.Add(new C());//works fine
    }
}

class A { }

class B : A { }

class C : A { }

class Container<T,S> where T: class, S
{
    S[] s = new S[4];
    int currentIndex = 0;

    public void Add(S t)
    {
        s[currentIndex] = t;
        currentIndex++;
    }
}

Sto barando un po 'perché non sto usando T in Container<> ... ma penso che sia comunque una relazione giusta.

    
posta P.Brian.Mackey 09.02.2013 - 22:24
fonte

3 risposte

2

Il problema non è che non è possibile memorizzare un riferimento a una C in quell'array (anche se sono abbastanza sicuro che non sia banale da implementare). Il problema è che, se lo permetti, un codice come questo (si noti che questo può essere distribuito su più metodi non correlati, a causa degli array che sono tipi di riferimento) diventa sbagliato:

B[] bs = new B[1];
A[] as = bs;
as[0] = new C();
B b = bs[0]; /* It's a C, but we use it as a B, and a C is not a B! */

Come va male? Bene, se non si accorge e assume un layout di oggetto errato, ottieni tutti i tipi di divertimento solitamente riservati al codice C, C ++ e unsafe :

  • Reinterpretare un int come float (o un riferimento a T come riferimento a U , per istanze ricorsive di tale divertimento).
  • Accesso alla memoria non inizializzata (o alla memoria di un altro oggetto)
  • Chiamare un metodo con il tipo o il numero di argomenti errati.
  • Utilizzare i padding byte come se avessero un significato.
  • Sovrascrittura dei metadati che il sistema di gestione della memoria / GC deve utilizzare.

In alternativa, è possibile ripristinare la sicurezza facendo in modo che tutto il codice che legge dagli array esegua controlli di tipo run time (al contrario di fare quel controllo quando scrive in l'array, come fa attualmente) e lanciare un'eccezione quando un oggetto ha un tipo inatteso. Questo è lento e si otterrebbe la stessa eccezione in un posto diverso (quando si accede al riferimento rispetto a quando lo si archivia), quindi non è stato "aggiustato" nulla. Questo perché non è possibile risolverlo, tale codice è intrinsecamente rotto. Il tuo Container mostrerebbe lo stesso problema se permettesse di ottenere riferimenti dal S[] (ad esempio, un indicizzatore) a meno che restituisca sempre il tipo più generale (un S , non un T ; altrimenti si verifica lo stesso problema).

    
risposta data 09.02.2013 - 22:52
fonte
2

Perché è quello che hai detto di fare. Questo è l'equivale di:

Girafe[] g = new Girafe[4];
Animal[] a = g;  // girafe array has been created by this time;

La tua affermazione è chiara su tutti i tipi, se lasci che il lato sinistro determini il tipo del giusto, potresti incontrare qualche problema. Prendere in considerazione ..

Girafe[] g = GetGArray():
Animal[] a = GetGArray();

È uguale a g? In caso contrario, sarebbe necessario creare un nuovo backing store, in tal caso, il compilatore dovrebbe determinare cosa succede in base al fatto che si tratti di una chiamata nuova o di una funzione. Ma poi c'è inlining ...

È più semplice fare esattamente ciò che dice la linea.

    
risposta data 09.02.2013 - 23:12
fonte
0

Come notato, gli array si legano a qualunque tipo venga specificato al momento della creazione. Il fatto che anche gli array mutabili possano essere passati covarmente è una conseguenza di alcune cose:

  1. Le matrici sono precedenti ai generici e fino a quando non sono stati aggiunti generici covarianti a .NET non c'era un tipo immutabile che potesse comportarsi in modo covariante, quindi gli array (che erano tutti mutabili) erano l'unica cosa che poteva essere covariante.

  2. Per ordinare in modo efficiente una matrice, è necessario essere in grado di leggere gli elementi dall'array e di scriverli nuovamente. Se si ha un riferimento di tipo Animal[] , anche se punta a Cat[] , si può senza alcuna possibilità di errore di runtime leggere qualsiasi elemento di quell'array in un riferimento di tipo Animal e successivamente memorizzarlo indietro alla matrice da cui proviene . La capacità di leggere gli elementi potrebbe non essere del tutto necessaria se Array contenesse alcuni metodi come CopyItem e SwapItem , ma essere in grado di leggere un elemento e scriverlo dopo aver eseguito molte altre modifiche a un array è più efficiente che sarebbe dover decomporre ogni sorta in una sequenza di scambi. Mentre è possibile progettare un'interfaccia covariante che consenta a un array o un'altra raccolta di essere ordinata in modo efficiente senza richiedere il codice di ordinamento per scrivere direttamente qualcosa nella raccolta stessa, è molto più semplice consentire semplicemente la covarianza dell'array.

  3. Un array conosce il tipo di elemento che deve contenere. Ciò gli consente di imporre la sicurezza del tipo in fase di esecuzione. Anche se le raccolte generiche aggiunte in .NET 2.0 conoscono anche il loro tipo di elemento, la maggior parte delle raccolte generiche in Java non lo fanno. Se si tenta di passare un ArrayList<Cat> al codice che si aspetta un ArrayList<Animal> , e se quest'ultimo codice ha provato ad aggiungere un Dog ad esso, non ci sarebbe modo in cui ArrayList<Cat> potrebbe sapere che è stato creato come ArrayList<Cat> e dovrebbe rifiutare la richiesta.

Se si dichiarano array che usano la sintassi come Array<Dog> myDogs = new Array<Dog>(34); piuttosto che Dog[] myDogs = new Dog[34]; , allora potrebbe essere possibile avere più tipi di riferimenti di array, così ad es. Array<Animal> myDogs = new Array<Dog>(34); non verrebbe compilato, ma SortableArray<Animal> myDogs = new Array<Dog>(34); lo farebbe. Tuttavia, né Java né .NET supportano più tipi di array.

    
risposta data 19.12.2013 - 19:22
fonte

Leggi altre domande sui tag