In C #, perché String è un tipo di riferimento che si comporta come un tipo di valore?


371

Una stringa è un tipo di riferimento anche se presenta la maggior parte delle caratteristiche di un tipo di valore, ad esempio essere immutabile e avere == sovraccarico per confrontare il testo anziché assicurarsi che facciano riferimento allo stesso oggetto.

Perché allora la stringa non è solo un tipo di valore?


Poiché per i tipi immutabili la distinzione è principalmente un dettaglio di implementazione (lasciando isda parte i test), la risposta è probabilmente "per ragioni storiche". Le prestazioni della copia non possono essere la ragione poiché non è necessario copiare fisicamente oggetti immutabili. Ora è impossibile cambiare senza rompere il codice che utilizza effettivamente i iscontrolli (o vincoli simili).
Elazar,

A proposito, questa è la stessa risposta per C ++ (sebbene la distinzione tra valore e tipi di riferimento non sia esplicita nella lingua), la decisione di std::stringcomportarsi come una raccolta è un vecchio errore che non può essere risolto ora.
Elazar,

Risposte:


333

Le stringhe non sono tipi di valore poiché possono essere enormi e devono essere archiviate nell'heap. I tipi di valore sono (ancora in tutte le implementazioni del CLR) memorizzati nello stack. Lo stack che alloca le stringhe romperebbe ogni sorta di cose: lo stack è solo 1 MB per 32 bit e 4 MB per 64 bit, dovresti mettere in scatola ogni stringa, incorrere in una penalità di copia, non puoi usare stringhe interne e usare la memoria sarebbe palloncino, ecc ...

(Modifica: aggiunto chiarimento sul fatto che l'archiviazione del tipo di valore è un dettaglio di implementazione, il che porta a questa situazione in cui abbiamo un tipo con semantica di valore che non eredita da System.ValueType. Grazie Ben.)


75
Sto facendo il nitpicking qui, ma solo perché mi dà l'opportunità di collegarmi a un post sul blog rilevante per la domanda: i tipi di valore non sono necessariamente memorizzati nello stack. È spesso vero in ms.net, ma non è affatto specificato dalla specifica CLI. La differenza principale tra valore e tipi di riferimento è che i tipi di riferimento seguono la semantica della copia per valore. Vedi blogs.msdn.com/ericlippert/archive/2009/04/27/… e blogs.msdn.com/ericlippert/archive/2009/05/04/…
Ben Schwehn,

8
@Qwertie: Stringnon è una dimensione variabile. Quando lo aggiungi, stai effettivamente creando un altro Stringoggetto, allocando nuova memoria per esso.
codekaizen,

5
Detto questo, una stringa avrebbe potuto, in teoria, essere un tipo di valore (una struttura), ma il "valore" non sarebbe stato altro che un riferimento alla stringa. I progettisti di .NET hanno naturalmente deciso di eliminare l'intermediario (la gestione della struttura era inefficiente in .NET 1.0 ed era naturale seguire Java, in cui le stringhe erano già definite come un tipo di riferimento, piuttosto che primitivo. Inoltre, se la stringa fosse un tipo di valore che poi la converte in oggetto richiederebbe che venga inscatolato, un'inutilità inutile).
Qwertie,

7
@codekaizen Qwertie ha ragione, ma penso che la formulazione sia confusa. Una stringa può avere dimensioni diverse rispetto a un'altra stringa e quindi, a differenza di un tipo di valore vero, il compilatore non è stato in grado di sapere in anticipo quanto spazio allocare per memorizzare il valore della stringa. Ad esempio, an Int32è sempre 4 byte, quindi il compilatore alloca 4 byte ogni volta che si definisce una variabile stringa. Quanta memoria dovrebbe allocare il compilatore quando incontra una intvariabile (se fosse un tipo di valore)? Comprendi che il valore non è stato ancora assegnato in quel momento.
Kevin Brock,

2
Spiacente, un errore di battitura nel mio commento che non posso risolvere ora; che avrebbe dovuto essere .... Ad esempio, un Int32è sempre 4 byte, quindi il compilatore alloca 4 byte ogni volta che si definisce una intvariabile. Quanta memoria dovrebbe allocare il compilatore quando incontra una stringvariabile (se fosse un tipo di valore)? Comprendi che il valore non è stato ancora assegnato in quel momento.
Kevin Brock,

57

Non è un tipo di valore perché le prestazioni (spazio e tempo!) Sarebbero terribili se fosse un tipo di valore e il suo valore dovesse essere copiato ogni volta che veniva passato e restituito da metodi, ecc.

Ha valore semantico per mantenere sano il mondo. Riesci a immaginare quanto sarebbe difficile codificare se

string s = "hello";
string t = "hello";
bool b = (s == t);

impostato bper essere false? Immagina quanto sarebbe difficile codificare qualsiasi applicazione.


44
Java non è noto per essere pithy.
Jason,

3
@Matt: esattamente. Quando sono passato a C # questo è stato un po 'confuso, dal momento che ho sempre usato (e ancora qualche volta) .equals (..) per confrontare le stringhe mentre i miei compagni di squadra hanno appena usato "==". Non ho mai capito perché non abbiano lasciato "==" per confrontare i riferimenti, anche se, se pensi, il 90% delle volte probabilmente vorrai confrontare il contenuto e non i riferimenti per le stringhe.
Juri,

7
@Juri: In realtà penso che non sia mai desiderabile controllare i riferimenti, poiché a volte new String("foo");e un altro new String("foo")può valutare nello stesso riferimento, che tipo di non è quello che ti aspetteresti da un newoperatore. (O puoi dirmi un caso in cui vorrei confrontare i riferimenti?)
Michael,

1
@Michael Bene, devi includere un confronto di riferimento in tutti i confronti per catturare il confronto con null. Un altro buon posto per confrontare i riferimenti con le stringhe è quando si confronta piuttosto che il confronto di uguaglianza. Due stringhe equivalenti, se confrontate, dovrebbero restituire 0. Il controllo per questo caso richiede comunque il tempo necessario per eseguire l'intero confronto, quindi non è una scorciatoia utile. Il controllo per ReferenceEquals(x, y)è un test veloce e puoi restituire immediatamente 0, e se mischiato con il tuo null-test non aggiunge nemmeno altro lavoro.
Jon Hanna,

1
... avere le stringhe come un tipo di valore di quello stile piuttosto che essere un tipo di classe significherebbe che il valore predefinito di a stringpotrebbe comportarsi come una stringa vuota (com'era nei sistemi pre.net) piuttosto che come riferimento null. In realtà, la mia preferenza sarebbe quella di avere un tipo di valore Stringche contenesse un tipo di riferimento NullableString, con il primo con un valore predefinito equivalente a String.Emptye il secondo con un valore predefinito di null, e con regole speciali di boxe / unboxing (tale che il boxing di default- valutato NullableStringdarebbe un riferimento a String.Empty).
supercat

26

La distinzione tra tipi di riferimento e tipi di valore è fondamentalmente un compromesso prestazionale nella progettazione della lingua. I tipi di riferimento hanno un certo sovraccarico di costruzione, distruzione e raccolta dei rifiuti, poiché sono creati nell'heap. D'altra parte, i tipi di valore hanno un sovraccarico nelle chiamate al metodo (se la dimensione dei dati è maggiore di un puntatore), poiché l'intero oggetto viene copiato anziché solo un puntatore. Poiché le stringhe possono essere (e in genere sono) molto più grandi delle dimensioni di un puntatore, sono progettate come tipi di riferimento. Inoltre, come ha sottolineato Servy, la dimensione di un tipo di valore deve essere nota al momento della compilazione, il che non è sempre il caso delle stringhe.

La questione della mutabilità è una questione separata. Sia i tipi di riferimento che i tipi di valore possono essere mutabili o immutabili. I tipi di valore sono in genere immutabili, poiché la semantica dei tipi di valori mutabili può essere fonte di confusione.

I tipi di riferimento sono generalmente mutabili, ma possono essere progettati come immutabili se ha senso. Le stringhe sono definite immutabili perché rendono possibili alcune ottimizzazioni. Ad esempio, se la stessa stringa letterale si verifica più volte nello stesso programma (che è abbastanza comune), il compilatore può riutilizzare lo stesso oggetto.

Quindi perché "==" è sovraccarico per confrontare le stringhe con il testo? Perché è la semantica più utile. Se due stringhe sono uguali per il testo, potrebbero essere o meno gli stessi riferimenti agli oggetti a causa delle ottimizzazioni. Quindi confrontare i riferimenti è piuttosto inutile, mentre confrontare i testi è quasi sempre quello che vuoi.

Parlando più in generale, Strings ha quella che viene definita semantica di valore . Questo è un concetto più generale rispetto ai tipi di valore, che è un dettaglio di implementazione specifico per C #. I tipi di valore hanno una semantica di valore, ma i tipi di riferimento possono anche avere una semantica di valore. Quando un tipo ha una semantica di valore, non si può davvero dire se l'implementazione sottostante è un tipo di riferimento o un tipo di valore, quindi è possibile considerare che un dettaglio dell'implementazione.


La distinzione tra tipi di valore e tipi di riferimento non riguarda affatto le prestazioni. Si tratta di stabilire se una variabile contiene un oggetto reale o un riferimento a un oggetto. Una stringa non potrebbe mai essere un tipo di valore perché la dimensione di una stringa è variabile; dovrebbe essere costante per essere un tipo di valore; le prestazioni non hanno quasi nulla a che fare con esso. Anche i tipi di riferimento non sono affatto costosi da creare.
Servito il

2
@Sevy: la dimensione di una stringa è costante.
JacquesB,

Perché contiene solo un riferimento a una matrice di caratteri, che è di dimensioni variabili. Avere un tipo di valore il cui unico "valore" reale era un tipo di riferimento sarebbe ancora più confuso, poiché avrebbe comunque una semantica di riferimento per tutti gli scopi intensivi.
Servito il

1
@Sevy: la dimensione di un array è costante.
JacquesB,

1
Dopo aver creato un array, la sua dimensione è costante, ma tutti gli array in tutto il mondo non hanno esattamente le stesse dimensioni. Questo è il mio punto. Affinché una stringa sia un tipo di valore, tutte le stringhe esistenti dovrebbero avere esattamente le stesse dimensioni, poiché è così che i tipi di valore sono progettati in .NET. Deve essere in grado di riservare spazio di archiviazione per tali tipi di valore prima di avere effettivamente un valore , quindi la dimensione deve essere conosciuta al momento della compilazione . Un tale stringtipo dovrebbe avere un buffer di caratteri di una dimensione fissa, che sarebbe sia restrittivo che altamente inefficiente.
Servizio

16

Questa è una risposta tardiva a una vecchia domanda, ma a tutte le altre risposte manca il punto, ovvero che .NET non aveva generici fino a .NET 2.0 nel 2005.

Stringè un tipo di riferimento anziché un tipo di valore perché era di fondamentale importanza per Microsoft garantire che le stringhe potessero essere archiviate nel modo più efficiente in raccolte non generiche , ad esempio System.Collections.ArrayList.

La memorizzazione di un tipo di valore in una raccolta non generica richiede una conversione speciale nel tipo objectchiamato boxing. Quando CLR inscatola un tipo di valore, avvolge il valore all'interno di a System.Objecte lo memorizza nell'heap gestito.

La lettura del valore dalla raccolta richiede l'operazione inversa che si chiama unboxing.

Sia il pugilato che il unboxing hanno costi non trascurabili: il boxing richiede un'allocazione aggiuntiva, il unboxing richiede il controllo del tipo.

Alcune risposte affermano erroneamente che string non avrebbe mai potuto essere implementato come tipo di valore poiché le sue dimensioni sono variabili. In realtà è facile implementare una stringa come una struttura di dati a lunghezza fissa usando una strategia di ottimizzazione delle stringhe di piccole dimensioni: le stringhe verrebbero archiviate direttamente in memoria come una sequenza di caratteri Unicode ad eccezione delle stringhe di grandi dimensioni che verrebbero archiviate come puntatore a un buffer esterno. Entrambe le rappresentazioni possono essere progettate per avere la stessa lunghezza fissa, ovvero la dimensione di un puntatore.

Se i generici fossero esistiti sin dal primo giorno, suppongo che avere una stringa come tipo di valore sarebbe probabilmente stata una soluzione migliore, con una semantica più semplice, un migliore utilizzo della memoria e una migliore localizzazione della cache. Un List<string>contenente solo piccole stringhe avrebbe potuto essere un singolo blocco contiguo di memoria.


Grazie per questa risposta! Ho esaminato tutte le altre risposte dicendo cose sulle allocazioni di heap e stack, mentre stack è un dettaglio di implementazione . Dopotutto, stringcontiene solo le sue dimensioni e un puntatore charall'array, quindi non sarebbe un "tipo di valore enorme". Ma questa è una ragione semplice e pertinente per questa decisione di progettazione. Grazie!
V0ldek,

8

Non solo le stringhe sono tipi di riferimento immutabili. Anche i delegati multi-cast. Ecco perché è sicuro scrivere

protected void OnMyEventHandler()
{
     delegate handler = this.MyEventHandler;
     if (null != handler)
     {
        handler(this, new EventArgs());
     }
}

Suppongo che le stringhe siano immutabili perché questo è il metodo più sicuro per lavorare con loro e allocare memoria. Perché non sono tipi di valore? Gli autori precedenti hanno ragione sulla dimensione dello stack, ecc. Aggiungerei anche che creando stringhe un tipo di riferimento consente di risparmiare sulla dimensione dell'assieme quando si utilizza la stessa stringa costante nel programma. Se lo definisci

string s1 = "my string";
//some code here
string s2 = "my string";

È probabile che entrambe le istanze della costante "my string" vengano allocate nel tuo assembly una sola volta.

Se desideri gestire le stringhe come il solito tipo di riferimento, inserisci la stringa in un nuovo StringBuilder (stringhe). Oppure usa MemoryStreams.

Se devi creare una libreria, in cui ti aspetti di passare enormi stringhe nelle tue funzioni, definisci un parametro come StringBuilder o come Stream.


1
Ci sono molti esempi di tipi di riferimento immutabili. E per quanto riguarda l'esempio di stringa, questo è praticamente garantito dalle attuali implementazioni - tecnicamente lo è per modulo (non per assemblaggio) - ma è quasi sempre la stessa cosa ...
Marc Gravell

5
Per quanto riguarda l'ultimo punto: StringBuilder non aiuta se si tenta di passare una stringa di grandi dimensioni (poiché è effettivamente implementata comunque come stringa) - StringBuilder è utile per manipolare una stringa più volte.
Marc Gravell

Intendevi delegare gestore, non hadler? (mi dispiace essere pignolo .. ma è molto vicino a un cognome (non comune) che conosco ....)
Pure.Krome

6

Inoltre, il modo in cui vengono implementate le stringhe (diverse per ogni piattaforma) e quando inizi a cucirle insieme. Come usare aStringBuilder . Alloca un buffer in cui copiare, una volta raggiunta la fine, alloca ancora più memoria, nella speranza che se si esegue una grande concatenazione, le prestazioni non saranno ostacolate.

Forse Jon Skeet può aiutarti qui?


5

È principalmente un problema di prestazioni.

Fare in modo che le stringhe si comportino come un valore simile aiuta a scrivere il codice, ma avere un tipo di valore avrebbe un enorme impatto sulle prestazioni.

Per uno sguardo approfondito, dai un'occhiata a un bell'articolo sulle stringhe nel framework .net.


3

In parole molto semplici, qualsiasi valore che ha una dimensione definita può essere trattato come un tipo di valore.


Questo dovrebbe essere un commento
ρяσѕρєя K,

più facile da capire per ppl nuovo a c #
LUNGO

2

Come si può dire che stringè un tipo di riferimento? Non sono sicuro che sia importante come viene implementato. Le stringhe in C # sono immutabili proprio in modo da non doverti preoccupare di questo problema.


È un tipo di riferimento (credo) perché non deriva da System.ValueType Da MSDN Note su System.ValueType: i tipi di dati sono separati in tipi di valore e tipi di riferimento. I tipi di valore sono allocati in pila o allocati in linea in una struttura. I tipi di riferimento sono allocati in heap.
Davy8,

Entrambi i tipi di riferimento e di valore sono derivati ​​dall'oggetto classe di base finale. Nei casi in cui è necessario che un tipo di valore si comporti come un oggetto, un wrapper che fa sembrare il tipo di valore simile a un oggetto di riferimento viene allocato sull'heap e il valore del tipo di valore viene copiato in esso.
Davy8,

Il wrapper è contrassegnato in modo che il sistema sappia che contiene un tipo di valore. Questo processo è noto come boxe e il processo inverso è noto come unboxing. Boxe e unboxing consentono a qualsiasi tipo di essere trattato come un oggetto. (Nel sito posteriore, probabilmente avrebbe dovuto solo essere collegato all'articolo.)
Davy8

2

In realtà le stringhe hanno pochissime somiglianze con i tipi di valore. Per i principianti, non tutti i tipi di valore sono immutabili, è possibile modificare il valore di un Int32 tutto ciò che si desidera e sarebbe comunque lo stesso indirizzo nello stack.

Le stringhe sono immutabili per un'ottima ragione, non hanno nulla a che fare con il fatto che sono un tipo di riferimento, ma hanno molto a che fare con la gestione della memoria. È solo più efficiente creare un nuovo oggetto quando cambia la dimensione della stringa che spostare le cose sull'heap gestito. Penso che stai mescolando tipi di valore / riferimento e concetti di oggetti immutabili.

Per quanto riguarda "==": Come hai detto, "==" è un sovraccarico dell'operatore, e di nuovo è stato implementato per un'ottima ragione per rendere il framework più utile quando si lavora con le stringhe.


Mi rendo conto che i tipi di valore non sono per definizione immutabili, ma la maggior parte delle migliori pratiche sembra suggerire che dovrebbero esserlo quando si crea il proprio. Ho detto caratteristiche, non proprietà dei tipi di valore, il che per me significa che spesso i tipi di valore mostrano questi, ma non necessariamente per definizione
Davy8

5
@WebMatrix, @ Davy8: i tipi primitivi (int, double, bool, ...) sono immutabili.
Jason,

1
@Jason, ho pensato che il termine immutabile si applica principalmente agli oggetti (tipi di riferimento) che non possono cambiare dopo l'inizializzazione, come le stringhe quando il valore delle stringhe cambia, viene creata internamente una nuova istanza di una stringa e l'oggetto originale rimane invariato. Come si applica ai tipi di valore?
WebMatrix

8
In qualche modo, in "int n = 4; n = 9;", non è che la tua variabile int sia "immutabile", nel senso di "costante"; è che il valore 4 è immutabile, non cambia in 9. La variabile int "n" ha prima un valore di 4 e poi un valore diverso, 9; ma i valori stessi sono immutabili. Francamente, per me questo è molto vicino al wtf.
Daniel Daranas,

1
+1. Sono stufo di sentire che "le stringhe sono come tipi di valore" quando semplicemente non lo sono.
Jon Hanna,

1

Non è così semplice come le stringhe sono costituite da array di caratteri. Guardo le stringhe come matrici di caratteri []. Pertanto si trovano nell'heap perché la posizione della memoria di riferimento è archiviata nello stack e punta all'inizio della posizione della memoria dell'array nell'heap. La dimensione della stringa non è nota prima di essere allocata ... perfetta per l'heap.

Ecco perché una stringa è davvero immutabile perché quando la cambi anche se ha le stesse dimensioni il compilatore non lo sa e deve allocare un nuovo array e assegnare caratteri alle posizioni nell'array. Ha senso se pensi alle stringhe come a un modo in cui le lingue ti proteggono dal dover allocare memoria al volo (leggi C come la programmazione)


1
"la dimensione della stringa non è nota prima che sia allocata" - questo non è corretto nel CLR.
codekaizen,

-1

A rischio di ottenere un altro misterioso down-voto ... il fatto che molti menzionino lo stack e la memoria rispetto ai tipi di valore e ai tipi primitivi è perché devono inserirsi in un registro nel microprocessore. Non è possibile eseguire il push o il pop di qualcosa nello / dallo stack se sono necessari più bit di quanti ne abbia un registro .... le istruzioni sono, ad esempio, "pop eax", poiché eax è largo 32 bit su un sistema a 32 bit.

I tipi primitivi a virgola mobile sono gestiti dall'FPU, che è largo 80 bit.

Tutto questo è stato deciso molto prima che esistesse un linguaggio OOP per offuscare la definizione di tipo primitivo e presumo che tipo di valore sia un termine che è stato creato appositamente per i linguaggi OOP.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.