Valore implicito rispetto alla semantica del tipo di riferimento

3

Questa domanda è stata fatta prima qui Perché le structs e le classi separano i concetti in C #? , ma sono interessato a un aspetto specifico di questo aspetto a cui non è stata data risposta.

Sto cercando di capire perché i progettisti di linguaggio per C # e Swift (e forse altri) hanno optato per una progettazione linguistica che promuova implicitamente la definizione di un tipo come un tipo di valore (struct) rispetto a un tipo di riferimento (classe).

Se un linguaggio (come C ++) supporta solo la definizione di tipi composti in un modo, ad esempio con la parola chiave class, ma consente alle istanze di questo tipo di essere associate a una variabile per valore o per riferimento, ciò consente al consumatore del tipo di decidere se usare semantica del valore o semantica di riferimento.

Una caratteristica spesso citata del buon design del linguaggio è la sua capacità di trasmettere sinteticamente il maggior significato possibile in un modo locale. Eppure, questi linguaggi moderni sembrano nascondere la semantica del valore rispetto alla ref quando li usano. Esplorando questa asserzione con due esempi:

1) Chiamare un metodo:

Considera un metodo che opera su un array in C #. Questo array viene passato come argomento al metodo. Poiché gli array in C # sono tipi di riferimento, il metodo può scegliere di mutare l'array direttamente o creare un nuovo array mentre si opera su copie dei valori dell'array originale. Immaginiamo che in entrambi i casi, il metodo restituisca il riferimento a tale matrice al chiamante per supportare il concatenamento del metodo. Dato questo scenario, se il nome del metodo non fa riferimento a mutazioni o no, la strategia adottata non è facilmente conoscibile dall'API (cioè senza testare o leggere la documentazione).

Ovviamente nell'esempio sopra, l'implementatore del metodo in generale non dovrebbe modificare l'array originale senza una buona ragione. Ma il punto è che può farlo e il chiamante non può saperlo solo scrivendo la chiamata al metodo.

Viceversa, se il design del linguaggio assume semantica di valore ovunque (anche se potrebbe passare dei riferimenti per motivi di prestazioni sotto la copertura, questo è un problema di implementazione), l'esempio sopra non sarebbe possibile. Se l'implementatore del metodo desidera mutare la matrice originale, richiederebbe che il parametro sia esplicitamente contrassegnato come riferimento alla matrice, comunicando un strong suggerimento al chiamante che la matrice verrà modificata.

2) Variabili locali:

Considera una classe di persone di base con membri tipici come nome, data di nascita, ecc.

var p1 = new Person();
p1.name = "Tom";
var p2 = p1;     //Note I would never do this but I'm trying to keep the example trivial for brevity.
p2.name = "Peter";

Poiché Person è una classe, viene creata una sola Persona a cui fa riferimento sia p1 che p2 . Pertanto, in un esempio più complesso, questa mutazione indiretta di p1 potrebbe non essere intenzionale. Tuttavia, se Person è stato dichiarato come struct, verrà creata una copia e p1 avrebbe mantenuto il nome di "Tom". Di nuovo, questo richiede all'utente del tipo di sapere se il tipo è stato definito come una struct o una classe - che le informazioni non sono prontamente disponibili nel contesto locale.

Se la lingua supporta solo la semantica del valore per impostazione predefinita con riferimenti espliciti in uso, questo tipo di errore non sarebbe possibile. Le cose sarebbero ancora più chiare se la lingua richiedesse un operatore diverso per accedere ai membri per refs vs. vals (ad esempio -> operator / . operator).

var p1 = new Person();
p1.name = "Tom"; 
var p2 = &p1;
p2->name = "Peter";    //It is pretty clear that we are using a reference here because the language forces a different operator.

Quindi, di nuovo, data la premessa di un buon linguaggio == linguaggio chiaro / non ambiguo, perché i progettisti di linguaggi moderni ritengono che sia meglio mettere semantica di tipo reference vs value nelle mani del type designer invece del tipo consumer?

    
posta Dragonspell 30.03.2016 - 21:16
fonte

5 risposte

1

If a language (like C++) only supports defining compound types in one manner, say with the class keyword, but allows instances of this type to be bound to a variable either by value or by reference, this lets the type's consumer decide whether to use value semantics vs. reference semantics.

Puoi farlo in C # semplicemente usando classi invece di strutture.

Since arrays in C# are reference types, the method could either choose to mutate the array directly or to build a new array while operating on copies of the original array's values. Imagine that in both cases, the method would return the reference to that array to the caller in order to support method chaining. Given this scenario, if the method name doesn't hint at mutation or not, the adopted strategy is not readily knowable from the API

Quando si concatenano i metodi, la buona pratica in C # stabilisce che l'array restituito è sempre una copia, non è un riferimento alla matrice originale. Il metodo di concatenamento viene sempre affrontato da una prospettiva immutabile . In altre parole, se scrivo il mio metodo nello stile concatenato dal metodo, dovrei aspettarmi una copia dell'array da restituire, non un riferimento alla matrice originale.

Se volessi mettere in mutazione l'array in posizione, passerei semplicemente l'array per riferimento alla mia funzione di muting e return void .

public void TransformArray(T[] array);

Se, d'altro canto, stavo dichiarando una semantica immutabile (cioè copia), restituirei una nuova matrice.

public T[] TransformArray(T[] array)

However, if Person was declared as a struct...

In generale, le sole cose che sono dichiarate come strutture in C # sono cose che molto probabilmente hanno semantica del valore solo , cioè tipi primitivi. Ecco un esempio:

struct Complex
{
    double Real;
    double Imaginary;
    double Magnitude;
    double Phase;
}

È tanto grande quanto un struct ottiene, a meno che tu non stia facendo qualcosa come lanciare strutture dati complesse in un flusso binario, un processo che richiede bit e allineamento di parole (per le quali le strutture sono adattate in modo univoco).

    
risposta data 30.03.2016 - 21:27
fonte
1

So again, given the premise of good language == clear/unambiguous language, why do modern language designers feel that it is better to put reference vs. value type semantics in the hands of the type designer instead of the type consumer?

Perché metterlo nelle mani del tipo consumatore rende la lingua meno chiara e perché non è l'unica premessa coinvolta nella progettazione del linguaggio.

In primo luogo, chiarezza. Il problema con il permettere agli utenti di specificare il valore pass by ref / value è che ora si ha inconsistenza. Devi ricordare in che modo Foo è stato dichiarato questa volta . L'argomento è che tale incoerenza nuoce alla chiarezza della lingua e aggiunge un carico cognitivo allo sviluppatore.

Perché altro? La struttura dei tipi C # si differenzia a seconda che siano di riferimento o tipi di valore per facilitare il marshalling (per i tipi di valore) o la spedizione (per riferimento). In particolare per C #, gli operatori di uguaglianza e codice hash generati automaticamente differiscono a seconda della modalità in cui ti trovi. Se si consente la modalità di passaggio, non è possibile ottimizzare il compilatore per questa roba.

E alla fine - non ha molta importanza. Nel moderno C #, i tipi di valore sono in gran parte disapprovati a favore di tipi di riferimento immutabili. Anche al loro apice, forse 1 su 50 tipi erano in realtà tipi di valore. Perché complicare ulteriormente la lingua per qualcosa che nessuno davvero userà?

    
risposta data 30.03.2016 - 22:19
fonte
1

Nel secondo esempio, quali sono i tipi di p1 e p2? Sono uguali o sono diversi? È consentito utilizzarne uno al posto di un altro? La mia comprensione (ad esempio dall'osservazione che la sintassi diversa viene utilizzata per accedere ai membri della classe) è che i tipi sono diversi e non possono sostituirsi a vicenda.

Se questo è davvero il caso, il motivo più essenziale per mantenere la differenza tra semantica di riferimento e valore nelle mani di type designer è il riutilizzo. Con questo approccio, non è necessario disporre di codice diverso per gestire i tipi di riferimento o di valore, possono essere utilizzati in modo uniforme. Ad esempio, un metodo generico (in una sintassi immaginaria)

void sort (data: Array [T -> Comparable])

consente di ordinare una matrice di elementi comparabili che possono essere di riferimento o di valore. Con la distinzione dal lato del tipo di consumatore richiederebbero due metodi:

void sort (data: Array [T -> Comparable])
void sort (data: Array [&T -> Comparable])

o qualcosa del genere.

Inoltre, in quest'ultimo caso è difficile evitare l'uso di parametri di tipo generici formali e avere solo

void sort (data: Array [Comparable])

potrebbe essere OK nel primo caso, a seconda delle regole di digitazione.

    
risposta data 31.03.2016 - 21:54
fonte
0

È una reazione esagerata all'affettatura. La tranciatura non avviene a nessuno se non ai principianti, a meno che non lo facciate intenzionalmente, ma la gente sente comunque il bisogno di vietarla nelle loro lingue. In genere ciò viene ottenuto forzando la separazione dei tipi di valore e implementando regole separate per essi, ad esempio in C # non è possibile avere strutture che ereditano da altre strutture.

L'intero punto della separazione è di limitare i tipi di valore.

Tutto questo è così che il loro primo giorno, le persone non possono accidentalmente tagliare.

    
risposta data 30.03.2016 - 21:28
fonte
0

La semantica di riferimento / valore è messa nelle mani del type designer perché questo rende il linguaggio molto più semplice per il consumatore dei tipi. In pratica, il consumatore non ha bisogno di capire la sottile differenza tra il valore e i tipi di riferimento, ma può trattarli allo stesso modo, il che rende il linguaggio molto più concettualmente (e sintatticamente).

In pratica , C # supporta solo semantica di riferimento. È vero, alcuni tipi come gli interi non vengono (sempre) creati nell'heap, ma questo è un dettaglio di implementazione - non ci sarebbero differenze osservabili (eccetto le prestazioni) a livello di linguaggio se gli interi sono sempre creati nell'heap come gli altri oggetti.

La differenza non è osservabile perché tutti i tipi di valore predefiniti (interi, Point ecc.) sono immutabili. Se i tipi di valore fossero mutabili, sarebbe possibile osservare la differenza poiché le modifiche non sono condivise allo stesso modo delle istanze di classe.

Ora c'è una scappatoia in questa logica dato che sono in grado di definire i tuoi struct s che sono mutabili. Tuttavia, la definizione di struct s è considerata una funzione avanzata ed è considerata una cattiva pratica definire% mutable struct s. (Il tuo esempio (2) è possibile solo se Person è un% mutabile% co_de.)

Quindi, a meno che tu non ti spari deliberatamente nel piede creando un tipo di valore mutabile, C # ha una semantica singola e consistente per copiare e passare: gli oggetti sono condivisi.

Quindi perché avere dei tipi di valore nella lingua? Questo è un compromesso di prestazioni. Sarebbe troppo sovraccarico per creare valori interi sull'heap (e doverli raccogliere di nuovo). I tipi piccoli, usati frequentemente (immutabili) sono definiti come tipi di valore che consentono alcune ottimizzazioni. A volte si dice che i tipi di valore sono posti nello stack (a differenza dell'heap), ma credo che questa sia una semplificazione e potrebbero essere ottimizzati per esistere puramente in un registro del processore.

Bottom line: il tipo consumer non ha la scelta tra trattare un tipo come riferimento o tipo di valore perché il linguaggio diventa molto più semplice concettualmente e sintatticamente se esiste un singolo modello coerente.

    
risposta data 31.03.2016 - 12:57
fonte

Leggi altre domande sui tag