2012-10-24 8 views
21

In base allo specification, le stringhe utilizzate come chiave per un hash vengono duplicate e bloccate. Altri oggetti mutabili non sembrano avere una considerazione così speciale. Ad esempio, con un tasto array, è possibile quanto segue.Perché una chiave di stringa per un hash è congelata?

a = [0] 
h = {a => :a} 
h.keys.first[0] = 1 
h # => {[1] => :a} 
h[[1]] # => nil 
h.rehash 
h[[1]] # => :a 

D'altra parte, una cosa simile non può essere eseguita con una chiave di stringa.

s = "a" 
h = {s => :s} 
h.keys.first.upcase! # => RuntimeError: can't modify frozen String 

Perché la stringa progettata per essere diversa da altri oggetti mutabili quando si tratta di un tasto cancelletto? C'è qualche caso d'uso in cui questa specifica diventa utile? Quali altre conseguenze ha questa specifica?


In realtà ho un caso d'uso in cui l'assenza di tali specifiche speciali sulle stringhe può essere utile. Cioè, ho letto con la gemma yaml un file YAML scritto manualmente che descrive un hash. le chiavi possono essere stringhe e vorrei consentire la noncuranza tra maiuscole e minuscole nel file YAML originale. Quando leggo un file, potrei avere un hash come questo:

h = {"foo" => :foo, "Bar" => :bar, "BAZ" => :baz} 

E voglio di normalizzare le chiavi in ​​minuscolo per ottenere questo:

h = {"foo" => :foo, "bar" => :bar, "baz" => :baz} 

facendo qualcosa di simile a questo:

h.keys.each(&:downcase!) 

ma che restituisce un errore per il motivo spiegato sopra.

+0

Sembra, per il mio scopo, il meglio che posso fare è 'h.keys.each {| s | h.store (s.downcase, h.delete (s))} '. – sawa

+0

Posso solo supporre al "perché". Poiché le stringhe rappresentano un caso d'uso più comune degli array, sospetto che il congelamento di una stringa sia più semplice da implementare. Se conoscessi Perl, guarderei se Ruby sta cercando di essere coerente con Perl nel suo comportamento hash. Se fossi esperto in giapponese, guarderei quando il congelamento delle chiavi è stato implementato, e vedere se questo è il risultato di una segnalazione di bug o di una discussione su una mailing list (presumibilmente in giapponese per qualcosa così presto nella storia di Ruby). –

+1

@AndrewGrimm [Here] (http://doc.ruby-lang.org/ja/1.9.2/class/Hash.html) dice che gli array e gli hash non rendono le chiavi valide per un hash perché possono essere modificati, e le stringhe sono bloccate in modo da non dover chiamare rehash. Coerente con la risposta di Steenslag. – sawa

risposta

20

In breve, è solo Ruby che cerca di essere gentile.

Quando si immette una chiave in un hash, viene calcolato un numero speciale, utilizzando il metodo hash della chiave. L'oggetto Hash usa questo numero per recuperare la chiave. Ad esempio, se chiedi quale sia il valore di h['a'], l'Hash chiama il metodo hash della stringa 'a' e controlla se ha un valore memorizzato per quel numero.Il problema sorge quando qualcuno (tu) muta l'oggetto stringa, quindi la stringa 'a' ora è qualcos'altro, diciamo 'aa'. L'hash non ha trovato un numero di hash per "aa".

I tipi più comuni di chiavi per gli hash sono stringhe, simboli e numeri interi. I simboli e gli interi sono immutabili, ma le stringhe no. Ruby cerca di proteggerti dal comportamento confuso descritto sopra dupping e congelando le chiavi di stringa. Immagino che non sia fatto per altri tipi perché potrebbero esserci effetti collaterali negativi (pensate a grandi matrici).

+0

Grazie per aver risposto alla parte teorica della domanda. –

4

Vedere this thread on the ruby-core mailing list per una spiegazione (stranamente, è stata la prima mail in cui mi sono imbattuto quando ho aperto la mailing list nella mia app di posta!).

non ho idea circa la prima parte della sua domanda, ma h Ecco una risposta pratica per il 2 ° parte:

new_hash = {} 
    h.each_pair do |k,v| 
    new_hash.merge!({k.downcase => v}) 
    end 

    h.replace new_hash 

c'è un sacco di permutazioni di questo tipo di codice,

Hash[ h.map{|k,v| [k.downcase, v] } ] 

essere un altro (e probabilmente siete a conoscenza di questi, ma a volte è meglio prendere la strada pratico :)

+1

Grazie! Molto utile – Bretticus

2

stai askin 2 domande diverse: teorica e pratica. Lain è stato il primo a rispondere, ma vorrei fornire quello che io considero una vera e propria, la soluzione più pigri alla tua domanda pratica:

Hash.new { |hsh, key| # this block get's called only if a key is absent 
    downcased = key.to_s.downcase 
    unless downcased == key # if downcasing makes a difference 
    hsh[key] = hsh[downcased] if hsh.has_key? downcased # define a new hash pair 
    end # (otherways just return nil) 
} 

Il blocco utilizzato con Hash.new costruttore viene invocato solo per quelle chiavi mancanti, che sono in realtà richiesto. La soluzione sopra accetta anche i simboli.

3

Le chiavi immutabili hanno senso in generale perché i loro codici hash saranno stabili.

Questo è il motivo per cui le stringhe sono appositamente convertiti, in questa parte del codice di MRI:

if (RHASH(hash)->ntbl->type == &identhash || rb_obj_class(key) != rb_cString) { 
    st_insert(RHASH(hash)->ntbl, key, val); 
} 
else { 
    st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key); 
} 

In poche parole, nel caso di stringa-chiave, st_insert2 viene passato un puntatore a una funzione che attiverà il dup e freeze.

Quindi se in teoria ha voluto sostenere le liste immutabili e hash immutabili come chiavi di hash, allora potremmo modificare il codice a qualcosa di simile:

VALUE key_klass; 
key_klass = rb_obj_class(key); 
if (key_klass == rb_cArray || key_klass == rb_cHash) { 
    st_insert2(RHASH(hash)->ntbl, key, val, freeze_obj); 
} 
else if (key_klass == rb_cString) { 
    st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key); 
} 
else { 
    st_insert(RHASH(hash)->ntbl, key, val); 
} 

Dove sarebbe definito freeze_obj come:

static st_data_t 
freeze_obj(st_data_t obj) 
{ 
    return (st_data_t)rb_obj_freeze((VALUE) obj); 
} 

In tal modo si risolverebbe l'incoerenza specifica osservata, in cui la chiave della matrice era mutabile. Tuttavia, per essere davvero coerenti, altri tipi di oggetti dovrebbero essere resi immutabili.

Non tutti i tipi, tuttavia. Ad esempio, non avrebbe senso congelare oggetti immediati come Fixnum perché in effetti esiste solo un'istanza di Fixnum corrispondente a ciascun valore intero. Questo è il motivo per cui solo String deve essere dotato di custodia speciale in questo modo, non Fixnum e Symbol.

Le stringhe sono un'eccezione speciale per la semplicità dei programmatori Ruby, poiché le stringhe vengono spesso utilizzate come chiavi hash.

contrario, la ragione per cui altri tipi di oggetti sono non congelati simili, che porta evidentemente a comportamenti incoerenti, è soprattutto una questione di convenienza per Matz & Società non supportare casi limite. In pratica, relativamente poche persone useranno un oggetto contenitore come un array o un hash come chiave hash. Quindi, se lo fai, sta a te congelare prima dell'inserimento.

Si noti che questo non è strettamente relativo alle prestazioni, perché l'atto di congelare un oggetto non immediato implica semplicemente l'inclinazione del bit FL_FREEZE sul campo bit basic.flags presente su ogni oggetto. Questa è ovviamente un'operazione a basso costo.

Parlando anche di prestazioni, si noti che se si intende utilizzare chiavi di stringa e si è in una sezione di codice critico per le prestazioni, è consigliabile bloccare le stringhe prima di eseguire l'inserimento. In caso contrario, viene attivato un dup duplex, operazione più costosa.

Aggiornamento @sawa ha sottolineato che lasciare il tuo array-chiave semplicemente congelato significa la matrice originale potrebbe essere al di fuori inaspettatamente immutabili del contesto d'uso chiave, che potrebbe anche essere una brutta sorpresa (anche se OTOH servirebbe destro per usare un array come una chiave hash, davvero).Se si suppone quindi che dup + freeze sia la via d'uscita, si incorre in un costo di prestazioni evidente. Nella terza mano, lasciatelo completamente scongelato, e ottenete la stranezza dell'OP originale. Stranità tutt'intorno. Un altro motivo per Matz e altri è di rinviare questi casi limite al programmatore.

+1

Il blocco della chiave originale senza la duplicazione sarebbe confuso. La duplicazione sarebbe indispensabile se una chiave verrà automaticamente congelata. Anche se il congelamento è economico, la duplicazione di un array, ecc. È costoso e, quindi, sembra che si tratti di un problema di prestazioni. Il tuo ultimo paragrafo è informativo. Sei sicuro che, se una stringa viene congelata dall'inizio, non verrebbe duplicata se usata come chiave hash? – sawa

+1

Per essere sicuro che sia così funziona, sì puoi vederlo qui: 'if (OBJ_FROZEN (orig)) return orig;' all'inizio di 'rb_str_new_frozen()', attualmente posizionato qui: github.com/ruby/ ruby/blob/trunk/string.C# L673 – manzoid

+1

Non sono necessariamente d'accordo sul fatto che "la duplicazione sarebbe un must" ... se il comportamento coerente per l'impostazione delle chiavi hash fosse che tutti erano semplicemente congelati, quindi persone che facevano insolito cose come provare ad usare un array come chiave e poi a mutarlo più tardi, scoprirebbero rapidamente che quell'uso non funziona, quando il tentativo di aggiornamento falliva a voce alta. La coerenza sarebbe probabilmente utile a volte. Ora, sicuramente vedo da dove vieni anche tu ... Sembra solo discutibile su cosa ottimizzare per: coerenza, prestazioni, protezione dei programmatori dalle conseguenze di fare cose strane, ecc. – manzoid

Problemi correlati