Dato che le stringhe sono immutabili in .NET, mi chiedo perché siano state progettate in modo tale da string.Substring()
richiedere tempo O ( substring.Length
) invece di O(1)
?
cioè quali sono stati i compromessi, se ce ne sono?
Dato che le stringhe sono immutabili in .NET, mi chiedo perché siano state progettate in modo tale da string.Substring()
richiedere tempo O ( substring.Length
) invece di O(1)
?
cioè quali sono stati i compromessi, se ce ne sono?
Risposte:
AGGIORNAMENTO: questa domanda mi è piaciuta così tanto, l'ho solo bloggata. Vedi Stringhe, immutabilità e persistenza
La risposta breve è: O (n) è O (1) se n non diventa grande. La maggior parte delle persone estrae minuscole sottostringhe da minuscole stringhe, quindi il modo in cui la complessità cresce asintoticamente è completamente irrilevante .
La lunga risposta è:
Una struttura di dati immutabile costruita in modo tale che le operazioni su un'istanza consentano il riutilizzo della memoria dell'originale con solo una piccola quantità (in genere O (1) o O (lg n)) di copia o nuova allocazione è chiamata "persistente" struttura dei dati immutabile. Le stringhe in .NET sono immutabili; la tua domanda è essenzialmente "perché non sono persistenti"?
Perché quando si osservano operazioni che vengono in genere eseguite su stringhe nei programmi .NET, in tutti i modi rilevanti non è affatto peggio semplicemente creare una stringa completamente nuova. La spesa e la difficoltà di costruzione di una struttura di dati persistente complessa non si ripagano da soli.
Le persone in genere usano la "sottostringa" per estrarre una stringa corta - diciamo, dieci o venti caratteri - da una stringa un po 'più lunga - forse un paio di centinaia di caratteri. Hai una riga di testo in un file separato da virgola e vuoi estrarre il terzo campo, che è un cognome. La linea sarà lunga forse circa duecento caratteri, il nome sarà una dozzina. L'allocazione delle stringhe e la copia della memoria di cinquanta byte è sorprendentemente veloce sull'hardware moderno. Che fare una nuova struttura di dati costituita da un puntatore a mezzo di una stringa esistente più un intervallo è inoltre sorprendentemente veloce è irrilevante; "abbastanza veloce" è per definizione abbastanza veloce.
Le sottostringhe estratte sono in genere di piccole dimensioni e di breve durata; il garbage collector li recupererà presto e non hanno occupato molto spazio sul mucchio in primo luogo. Quindi anche usare una strategia persistente che incoraggia il riutilizzo della maggior parte della memoria non è una vittoria; tutto ciò che hai fatto è rallentare il tuo garbage collector perché ora deve preoccuparsi di gestire i puntatori interni.
Se le operazioni di sottostringa che le persone facevano in genere sulle stringhe erano completamente diverse, sarebbe logico seguire un approccio persistente. Se in genere le persone avessero stringhe di milioni di caratteri e stessero estraendo migliaia di sottostringhe sovrapposte con dimensioni nell'intervallo di centomila caratteri e tali sottostringhe vivessero a lungo sull'heap, sarebbe perfettamente logico procedere con una sottostringa persistente approccio; sarebbe inutile e sciocco non farlo. Ma la maggior parte dei programmatori line-of-business non fa nulla di simile a quel tipo di cose vagamente. .NET non è una piattaforma su misura per le esigenze del Progetto genoma umano; I programmatori di analisi del DNA devono risolvere ogni giorno problemi con quelle caratteristiche di utilizzo delle stringhe; le probabilità sono buone che tu non lo faccia. I pochi che costruiscono le proprie strutture di dati persistenti che corrispondono strettamente ai loro scenari di utilizzo.
Ad esempio, il mio team scrive programmi che eseguono analisi al volo del codice C # e VB durante la digitazione. Alcuni di questi file di codice sono enormi e quindi non possiamo eseguire la manipolazione di stringhe O (n) per estrarre sottostringhe o inserire o eliminare caratteri. Abbiamo costruito una serie di strutture di dati immutabili persistenti per rappresentare modifiche a un buffer di testo che ci permette di rapido ed efficiente riutilizzare la maggior parte dei dati di stringa esistenti e le analisi lessicali e sintattici esistenti su di un montaggio tipico. Questo è stato un problema difficile da risolvere e la sua soluzione è stata strettamente adattata al dominio specifico della modifica del codice C # e VB. Non sarebbe realistico aspettarsi che il tipo di stringa incorporato risolva questo problema per noi.
string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...
o altre versioni di esso. Intendo leggere un intero file, quindi elaborare le varie parti. Questo tipo di codice sarebbe notevolmente più veloce e richiederebbe meno memoria se una stringa fosse persistente; avresti sempre esattamente una copia del file in memoria invece di copiare ogni riga, quindi le parti di ogni riga mentre la elabori. Tuttavia, come ha detto Eric, questo non è il tipico caso d'uso.
String
è implementato come una struttura di dati persistente (non specificato negli standard, ma tutte le implementazioni che conosco lo fanno).
Proprio perché le stringhe sono immutabili, è .Substring
necessario effettuare una copia di almeno una parte della stringa originale. Fare una copia di n byte dovrebbe richiedere O (n) tempo.
Come pensi che copieresti un mucchio di byte in tempo costante ?
EDIT: Mehrdad suggerisce di non copiare affatto la stringa, ma di mantenere un riferimento a un pezzo di essa.
Considera in .Net, una stringa multi-megabyte, su cui qualcuno chiama .SubString(n, n+3)
(per qualsiasi n nel mezzo della stringa).
Ora, la stringa INTERA non può essere Garbage Collected solo perché un riferimento è trattenuto da 4 caratteri? Sembra un ridicolo spreco di spazio.
Inoltre, tenere traccia dei riferimenti alle sottostringhe (che possono anche trovarsi all'interno delle sottostringhe) e cercare di copiarla in momenti ottimali per evitare di sconfiggere il GC (come descritto sopra), rende il concetto un incubo. È molto più semplice e più affidabile copiare .SubString
e mantenere il modello semplice e immutabile.
EDIT: ecco una buona lettura del pericolo di mantenere i riferimenti alle sottostringhe all'interno di stringhe più grandi.
memcpy
che è ancora O (n).
char*
sottostringa.
NULL
terminate. Come spiegato nel post di Lippert , i primi 4 byte contengono la lunghezza della stringa. Ecco perché, come sottolinea Skeet, possono contenere \0
personaggi.
Java (al contrario di .NET) offre due modi di fare Substring()
, puoi considerare se vuoi mantenere solo un riferimento o copiare un'intera sottostringa in una nuova posizione di memoria.
Il semplice .substring(...)
condivide l' char
array utilizzato internamente con l'oggetto String originale, che sarà quindi new String(...)
possibile copiare in un nuovo array, se necessario (per evitare di ostacolare la garbage collection di quello originale).
Penso che questo tipo di flessibilità sia l'opzione migliore per uno sviluppatore.
.substring(...)
.
Java faceva riferimento a stringhe più grandi, ma:
Sento che può essere migliorato però: perché non limitarsi a copiare in modo condizionale?
Se la sottostringa ha almeno la metà della dimensione del genitore, si può fare riferimento al genitore. Altrimenti si può semplicemente fare una copia. Ciò evita la perdita di molta memoria pur offrendo un vantaggio significativo.
char[]
(con diversi puntatori all'inizio e alla fine) alla creazione di un nuovo String
. Ciò dimostra chiaramente che l'analisi costi-benefici deve mostrare una preferenza per la creazione di un nuovo String
.
Nessuna delle risposte qui ha affrontato "il problema del bracketing", vale a dire che le stringhe in .NET sono rappresentate come una combinazione di un BStr (la lunghezza memorizzata "prima" del "puntatore) e un CStr (la stringa termina in un '\ 0').
La stringa "Hello there" è quindi rappresentata come
0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00
(se assegnato a a char*
in fixed
-statement il puntatore punta a 0x48.)
Questa struttura consente una rapida ricerca della lunghezza di una stringa (utile in molti contesti) e consente il passaggio del puntatore in API P / Invoke a Win32 (o altre) che prevedono una stringa con terminazione null.
Quando fai Substring(0, 5)
la regola "oh, ma ho promesso che ci sarebbe un carattere null dopo l'ultimo carattere" dice che devi fare una copia. Anche se alla fine avessi la sottostringa, non ci sarebbe posto per inserire la lunghezza senza corrompere le altre variabili.
A volte, però, vuoi davvero parlare del "centro della stringa" e non ti preoccupi necessariamente del comportamento P / Invoke. La ReadOnlySpan<T>
struttura aggiunta di recente può essere utilizzata per ottenere una sottostringa senza copia:
string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);
La ReadOnlySpan<char>
"sottostringa" memorizza la lunghezza in modo indipendente e non garantisce che ci sia un '\ 0' dopo la fine del valore. Può essere usato in molti modi "come una stringa", ma non è "una stringa" poiché non ha né caratteristiche BStr né CStr (e tanto meno entrambe). Se non si mai (direttamente) P / Invoke, allora non c'è molta differenza (a meno che l'API che si desidera chiamare non abbia un ReadOnlySpan<char>
sovraccarico).
ReadOnlySpan<char>
non può essere usato come campo di un tipo di riferimento, quindi c'è anche ReadOnlyMemory<char>
( s.AsMemory(0, 5)
), che è un modo indiretto di avere un ReadOnlySpan<char>
, quindi le stesse differenze da- string
esistono.
Alcune delle risposte / commenti sulle risposte precedenti hanno parlato del fatto che è dispendioso che il garbage collector debba tenere in giro una stringa di milioni di caratteri mentre si continua a parlare di 5 caratteri. Questo è esattamente il comportamento che puoi ottenere con l' ReadOnlySpan<char>
approccio. Se stai solo eseguendo brevi calcoli, l'approccio ReadOnlySpan è probabilmente migliore. Se hai bisogno di persistere per un po 'e manterrai solo una piccola percentuale della stringa originale, probabilmente fare una sottostringa corretta (per tagliare i dati in eccesso) è probabilmente meglio. C'è un punto di transizione da qualche parte nel mezzo, ma dipende dal tuo utilizzo specifico.