it-swarm.com.de

Ruby Style: So prüfen Sie, ob ein verschachteltes Hash-Element vorhanden ist

Betrachten Sie eine "Person", die in einem Hash gespeichert ist. Zwei Beispiele sind:

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 

Wenn die "Person" keine Kinder hat, ist das Element "Kinder" nicht vorhanden. Für Herrn Slate können wir also prüfen, ob er Eltern hat:

slate_has_children = !slate[:person][:children].nil?

Was ist, wenn wir nicht wissen, dass "Schiefer" ein Personenhash ist? Erwägen:

dino = {:pet => {:name => "Dino"}}

Wir können nicht mehr leicht nach Kindern suchen:

dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]' for nil:NilClass

Wie würden Sie also die Struktur eines Hashes überprüfen, insbesondere wenn er tief verschachtelt ist (sogar tiefer als die hier angegebenen Beispiele)? Vielleicht ist eine bessere Frage: Was ist der "Ruby-Weg", um dies zu tun?

53
Todd R

Der naheliegendste Weg, dies zu tun, besteht darin, einfach jeden Schritt des Weges zu überprüfen:

has_children = slate[:person] && slate[:person][:children]

Verwendung von .nil? ist wirklich nur erforderlich, wenn Sie false als Platzhalterwert verwenden. In der Praxis ist dies selten. Im Allgemeinen können Sie einfach testen, dass es existiert.

Update: Wenn Sie Ruby 2.3 oder höher verwenden, gibt es eine integrierte Dig -Methode, die die in dieser Antwort beschriebenen Schritte ausführt.

Wenn nicht, können Sie auch eine eigene Hash-Dig-Methode definieren, die dies erheblich vereinfachen kann:

class Hash
  def Dig(*path)
    path.inject(self) do |location, key|
      location.respond_to?(:keys) ? location[key] : nil
    end
  end
end

Diese Methode prüft jeden Schritt des Weges und vermeidet Stolpern bei Anrufen von Null. Für flache Strukturen ist der Nutzen etwas eingeschränkt, aber für tief verschachtelte Strukturen finde ich es von unschätzbarem Wert:

has_children = slate.Dig(:person, :children)

Sie können dies auch robuster gestalten, indem Sie beispielsweise prüfen, ob der Eintrag: children tatsächlich gefüllt ist:

children = slate.Dig(:person, :children)
has_children = children && !children.empty?
76
tadman

Mit Ruby 2.3 haben wir Unterstützung für den sicheren Navigationsoperator: https://www.Ruby-lang.org/de/news/2015/11/11/Ruby-2-3-0-preview1 -veröffentlicht/

has_children könnte jetzt geschrieben werden als: 

has_children = slate[:person]&.[](:children)

Dig wird auch hinzugefügt:

has_children = slate.Dig(:person, :children)
20
Mario Pérez

Eine andere Alternative:

dino.fetch(:person, {})[:children]
13
Cameron Martin

Sie können den andand gem verwenden:

require 'andand'

fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true

Weitere Erklärungen finden Sie unter http://andand.rubyforge.org/ .

4
paradigmatic

Traditionell musste man so etwas tun:

structure[:a] && structure[:a][:b]

Ruby 2.3 fügte jedoch eine Funktion hinzu, die diesen Weg eleganter gestaltet:

structure.Dig :a, :b # nil if it misses anywhere along the way

Es gibt einen Edelstein namens Ruby_Dig, der dies für Sie zurückpatcht.

2
DigitalRoss

Man könnte Hash mit dem Standardwert {} verwenden - leerer Hash. Zum Beispiel,

dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false

Das funktioniert auch mit bereits erstelltem Hash:

dino = {:pet=>{:name=>"Dino"}}
dino.default = {}
dino_has_children = !dino[:person][:children].nil? #=> false

Oder Sie können die Methode [] für die nil-Klasse definieren

class NilClass
  def [](* args)
     nil
   end
end

nil[:a] #=> nil
2
kirushik
dino_has_children = !dino.fetch(person, {})[:children].nil?

Beachten Sie, dass Sie in Rails auch Folgendes tun können:

dino_has_children = !dino[person].try(:[], :children).nil?   # 
def flatten_hash(hash)
  hash.each_with_object({}) do |(k, v), h|
    if v.is_a? Hash
      flatten_hash(v).map do |h_k, h_v|
        h["#{k}_#{h_k}"] = h_v
      end
    else
      h[k] = v
    end
  end
end

irb(main):012:0> fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
=> {:person=>{:name=>"Fred", :spouse=>"Wilma", :children=>{:child=>{:name=>"Pebbles"}}}}

irb(main):013:0> slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}

irb(main):014:0> flatten_hash(fred).keys.any? { |k| k.include?("children") }
=> true

irb(main):015:0> flatten_hash(slate).keys.any? { |k| k.include?("children") }
=> false

Dadurch werden alle Hashes in eine und dann in eine beliebige abgeflacht. Gibt "true" zurück, wenn ein Schlüssel vorhanden ist, der mit der untergeordneten Zeichenfolge "children" übereinstimmt. Dies kann auch hilfreich sein.

1
bharath

Hier können Sie eine gründliche Prüfung auf falsche Werte im Hash und alle verschachtelten Hashes durchführen, ohne dass der Affe die Ruby-Hash-Klasse patchen muss (BITTE machen Sie keinen Affen-Patch für die Ruby-Klassen. Dies sollten Sie jedoch niemals tun.) .

(Angenommen, Rails, obwohl Sie dies leicht ändern könnten, um außerhalb von Rails zu arbeiten.)

def deep_all_present?(hash)
  fail ArgumentError, 'deep_all_present? only accepts Hashes' unless hash.is_a? Hash

  hash.each do |key, value|
    return false if key.blank? || value.blank?
    return deep_all_present?(value) if value.is_a? Hash
  end

  true
end
1
josiah

Vereinfachung der obigen Antworten hier: 

Erstellen Sie eine rekursive Hash-Methode, deren Wert nicht wie folgt null sein kann.

def recursive_hash
  Hash.new {|key, value| key[value] = recursive_hash}
end

> slate = recursive_hash 
> slate[:person][:name] = "Mr. Slate"
> slate[:person][:spouse] = "Mrs. Slate"

> slate
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}
slate[:person][:state][:city]
=> {}

Wenn es Ihnen nichts ausmacht, leere Hashes zu erstellen, wenn der Wert für den Schlüssel nicht vorhanden ist :)

1
Abhi

Thks @tadman für die Antwort.

Für diejenigen, die Perfs wollen (und mit Ruby <2.3 hängen bleiben), ist diese Methode 2,5x schneller:

unless Hash.method_defined? :Dig
  class Hash
    def Dig(*path)
      val, index, len = self, 0, path.length
      index += 1 while(index < len && val = val[path[index]])
      val
    end
  end
end

und wenn Sie RubyInline verwenden, ist diese Methode 16x schneller:

unless Hash.method_defined? :Dig
  require 'inline'

  class Hash
    inline do |builder|
      builder.c_raw '
      VALUE Dig(int argc, VALUE *argv, VALUE self) {
        rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
        self = rb_hash_aref(self, *argv);
        if (NIL_P(self) || !--argc) return self;
        ++argv;
        return Dig(argc, argv, self);
      }'
    end
  end
end
0
gtournie

Sie können eine Kombination aus & und key? verwenden, es ist O(1) im Vergleich zu Dig, das O(n) ist stellt sicher, dass auf die Person zugegriffen wird, ohne dass NoMethodError: undefined method `[]' for nil:NilClass

fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)
0
aabiro

Sie können auch ein Modul definieren, das die Klammermethoden als Alias ​​bezeichnet, und die Ruby-Syntax verwenden, um geschachtelte Elemente zu lesen/schreiben. 

UPDATE: Anstatt die Klammer-Zugriffsmethoden zu überschreiben, fordern Sie die Hash-Instanz an, das Modul zu erweitern.

module Nesty
  def []=(*keys,value)
    key = keys.pop
    if keys.empty? 
      super(key, value) 
    else
      if self[*keys].is_a? Hash
        self[*keys][key] = value
      else
        self[*keys] = { key => value}
      end
    end
  end

  def [](*keys)
    self.Dig(*keys)
  end
end

class Hash
  def nesty
    self.extend Nesty
    self
  end
end

Dann können Sie tun:

irb> a = {}.nesty
=> {}
irb> a[:a, :b, :c] = "value"
=> "value"
irb> a
=> {:a=>{:b=>{:c=>"value"}}}
irb> a[:a,:b,:c]
=> "value"
irb> a[:a,:b]
=> {:c=>"value"}
irb> a[:a,:d] = "another value"
=> "another value"
irb> a
=> {:a=>{:b=>{:c=>"value"}, :d=>"another value"}}
0
Juan Matias

Ich weiß nicht, wie "Ruby" es ist (!), Aber das von mir geschriebene KeyDial gem lässt Sie dies grundsätzlich tun, ohne Ihre ursprüngliche Syntax zu ändern:

has_kids = !dino[:person][:children].nil?

wird:

has_kids = !dino.dial[:person][:children].call.nil?

Dies verwendet einige Tricksereien, um die wichtigsten Zugriffsaufrufe zu vermitteln. Bei call wird versucht, Dig die vorherigen Schlüssel von dino zu __zugeben, und wenn ein Fehler auftritt (wie gewünscht), wird nil zurückgegeben. nil? gibt dann natürlich true zurück.

0
Convincible

Sie können versuchen mit zu spielen

dino.default = {}

Oder zum Beispiel:

empty_hash = {}
empty_hash.default = empty_hash

dino.default = empty_hash

So kannst du anrufen

empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}
0
MBO

Gegeben

x = {:a => {:b => 'c'}}
y = {}

sie könnten x und y wie folgt überprüfen:

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
0
wedesoft