Kiedy używać symboli zamiast łańcuchów w Rubim?

98

Jeśli w moim skrypcie są co najmniej dwa wystąpienia tego samego ciągu, czy powinienem zamiast tego użyć symbolu?

Alan Coromano
źródło

Odpowiedzi:

175

TL; DR

Prostą zasadą jest używanie symboli za każdym razem, gdy potrzebujesz identyfikatorów wewnętrznych. W Ruby <2.2 używaj symboli tylko wtedy, gdy nie są generowane dynamicznie, aby uniknąć wycieków pamięci.

Pełna odpowiedź

Jedynym powodem, dla którego nie należy ich używać do identyfikatorów generowanych dynamicznie, są problemy z pamięcią.

To pytanie jest bardzo częste, ponieważ wiele języków programowania nie ma symboli, tylko ciągi znaków, a zatem ciągi są również używane jako identyfikatory w twoim kodzie. Powinieneś martwić się tym, jakie symbole mają być , a nie tylko, kiedy powinieneś ich używać . Symbole mają być identyfikatorami. Jeśli będziesz postępować zgodnie z tą filozofią, są szanse, że zrobisz wszystko dobrze.

Istnieje kilka różnic między implementacją symboli i łańcuchów. Najważniejszą rzeczą w symbolach jest to, że są niezmienne . Oznacza to, że ich wartość nigdy nie ulegnie zmianie. Z tego powodu symbole są tworzone szybciej niż łańcuchy, a niektóre operacje, takie jak porównywanie dwóch symboli, są również szybsze.

Fakt, że symbol jest niezmienny, pozwala Ruby na używanie tego samego obiektu za każdym razem, gdy odwołujesz się do symbolu, oszczędzając pamięć. Więc za każdym razem, gdy interpreter czyta :my_key, może pobrać go z pamięci, zamiast tworzyć instancję ponownie. Jest to mniej kosztowne niż każdorazowa inicjalizacja nowego łańcucha.

Możesz uzyskać listę wszystkich symboli, które zostały już utworzone za pomocą polecenia Symbol.all_symbols:

symbols_count = Symbol.all_symbols.count # all_symbols is an array with all 
                                         # instantiated symbols. 
a = :one
puts a.object_id
# prints 167778 

a = :two
puts a.object_id
# prints 167858

a = :one
puts a.object_id
# prints 167778 again - the same object_id from the first time!

puts Symbol.all_symbols.count - symbols_count
# prints 2, the two objects we created.

W przypadku wersji Ruby starszych niż 2.2, po utworzeniu wystąpienia symbolu pamięć ta nigdy nie będzie już wolna . Jedynym sposobem na zwolnienie pamięci jest ponowne uruchomienie aplikacji. Dlatego symbole są również główną przyczyną wycieków pamięci, jeśli są używane nieprawidłowo. Najprostszym sposobem wygenerowania wycieku pamięci jest użycie metody to_symna danych wejściowych użytkownika, ponieważ dane te zawsze się zmieniają, nowa część pamięci będzie używana na zawsze w wystąpieniu oprogramowania. Ruby 2.2 wprowadził moduł odśmiecania symboli , który uwalnia symbole generowane dynamicznie, więc wycieki pamięci generowane przez dynamiczne tworzenie symboli nie są już problemem.

Odpowiadając na Twoje pytanie:

Czy to prawda, że ​​muszę użyć symbolu zamiast ciągu, jeśli w mojej aplikacji lub skrypcie są co najmniej dwa takie same ciągi?

Jeśli szukasz identyfikatora, który ma być używany wewnętrznie w kodzie, powinieneś używać symboli. Jeśli drukujesz dane wyjściowe, powinieneś używać ciągów, nawet jeśli pojawia się więcej niż raz, nawet przydzielając dwa różne obiekty w pamięci.

Oto powód:

  1. Drukowanie symboli będzie wolniejsze niż drukowanie ciągów, ponieważ są one rzutowane na ciągi.
  2. Posiadanie wielu różnych symboli zwiększy ogólne użycie pamięci przez aplikację, ponieważ nigdy nie są one zwalniane. I nigdy nie używasz wszystkich ciągów z kodu w tym samym czasie.

Przypadek użycia autorstwa @AlanDert

@AlanDert: jeśli często używam czegoś takiego jak% input {type:: checkbox} w kodzie haml, czego powinienem użyć jako pola wyboru?

Ja tak.

@AlanDert: Ale żeby wydrukować symbol na stronie html, należy go przekonwertować na string, prawda? jaki jest sens jej używania?

Jaki jest typ wejścia? Identyfikator typu danych wejściowych, których chcesz użyć, lub coś, co chcesz pokazać użytkownikowi?

To prawda, że ​​w pewnym momencie stanie się kodem HTML, ale w momencie, gdy piszesz ten wiersz kodu, oznacza to, że jest identyfikatorem - identyfikuje, jakiego rodzaju pola wejściowego potrzebujesz. W związku z tym jest używany w kodzie w kółko i ma zawsze ten sam „ciąg” znaków co identyfikator i nie spowoduje wycieku pamięci.

To powiedziawszy, dlaczego nie ocenimy danych, aby sprawdzić, czy ciągi znaków są szybsze?

Oto prosty test porównawczy, który stworzyłem w tym celu:

require 'benchmark'
require 'haml'

str = Benchmark.measure do
  10_000.times do
    Haml::Engine.new('%input{type: "checkbox"}').render
  end
end.total

sym = Benchmark.measure do
  10_000.times do
    Haml::Engine.new('%input{type: :checkbox}').render
  end
end.total

puts "String: " + str.to_s
puts "Symbol: " + sym.to_s

Trzy wyjścia:

# first time
String: 5.14
Symbol: 5.07
#second
String: 5.29
Symbol: 5.050000000000001
#third
String: 4.7700000000000005
Symbol: 4.68

Tak więc używanie smbolów jest w rzeczywistości nieco szybsze niż używanie łańcuchów. Dlaczego? Zależy to od sposobu implementacji HAML. Musiałbym trochę zhakować kod HAML, aby zobaczyć, ale jeśli nadal będziesz używać symboli w koncepcji identyfikatora, twoja aplikacja będzie szybsza i niezawodna. Kiedy pojawią się pytania, porównaj je i uzyskaj odpowiedzi.

fotanus
źródło
@andrewcockerham Podany przez Ciebie link nie działa (błąd-404). Musisz usunąć ostatnie /(po strings) z linku. Tutaj jest: www.reactive.io/tips/2009/01/11/the-difference-between-ruby-‌ symbols-and-strings
Atul Khanduri
14

Mówiąc prościej, symbol to nazwa złożona ze znaków, ale niezmienna. Wręcz przeciwnie, łańcuch jest uporządkowanym kontenerem na znaki, których zawartość może się zmieniać.

Boris Stitnicky
źródło
4
+1. Symbole i ciągi to zupełnie inne rzeczy. Naprawdę nie ma żadnego zamieszania co do tego, którego użyć, chyba że został źle nauczony (tj. Błąd „symbol jest tylko niezmiennym ciągiem znaków”).
Jörg W Mittag
@ JörgWMittag: Dokładnie.
Boris Stitnicky
5
masz rację, ale nie odpowiadaj na zadane pytanie. OP myli ciągi znaków z symbolami, nie wystarczy powiedzieć, że to różne rzeczy - powinieneś pomóc mu zrozumieć, czym są podobne i czym różnią się
fotanus
1
@ JörgWMittag, które dzieje się w całej sieci, chyba że zajrzysz do dokumentacji lub masz szczęście, że znajdziesz ludzi, którym zależy na wyjaśnianiu rzeczy takimi, jakimi są naprawdę.
sargas
8
  1. Symbol Ruby to obiekt z porównaniem O (1)

Aby porównać dwa ciągi, potencjalnie musimy przyjrzeć się każdemu znakowi. W przypadku dwóch ciągów o długości N będzie to wymagało porównań N + 1 (które informatycy nazywają „czasem O (N)”).

def string_comp str1, str2
  return false if str1.length != str2.length
  for i in 0...str1.length
    return false if str1[i] != str2[i]
  end
  return true
end
string_comp "foo", "foo"

Ale ponieważ każde wystąpienie: foo odnosi się do tego samego obiektu, możemy porównać symbole, patrząc na identyfikatory obiektów. Możemy to zrobić za pomocą pojedynczego porównania (które informatycy określają jako „czas O (1)”).

def symbol_comp sym1, sym2
  sym1.object_id == sym2.object_id
end
symbol_comp :foo, :foo
  1. Symbol Ruby to etykieta w wyliczeniu w dowolnej formie

W C ++ możemy użyć „wyliczeń” do reprezentowania rodzin powiązanych stałych:

enum BugStatus { OPEN, CLOSED };
BugStatus original_status = OPEN;
BugStatus current_status  = CLOSED;

Ale ponieważ Ruby jest językiem dynamicznym, nie martwimy się o deklarowanie typu BugStatus lub śledzenie wartości prawnych. Zamiast tego reprezentujemy wartości wyliczenia jako symbole:

original_status = :open
current_status  = :closed

3. Symbol Ruby to stała, unikalna nazwa

W Rubim możemy zmienić zawartość łańcucha:

"foo"[0] = ?b # "boo"

Ale nie możemy zmienić zawartości symbolu:

:foo[0]  = ?b # Raises an error
  1. Symbol Ruby jest słowem kluczowym dla argumentu słowa kluczowego

Podczas przekazywania argumentów słów kluczowych do funkcji Rubiego określamy słowa kluczowe za pomocą symboli:

# Build a URL for 'bug' using Rails.
url_for :controller => 'bug',
        :action => 'show',
        :id => bug.id
  1. Symbol Ruby to doskonały wybór dla klucza z krzyżykiem

Zazwyczaj używamy symboli do reprezentowania kluczy tablicy skrótów:

options = {}
options[:auto_save]     = true
options[:show_comments] = false
Arun Kumar M
źródło
5

Oto dobry test porównawczy ciągów i symboli, który znalazłem w codecademy:

require 'benchmark'

string_AZ = Hash[("a".."z").to_a.zip((1..26).to_a)]
symbol_AZ = Hash[(:a..:z).to_a.zip((1..26).to_a)]

string_time = Benchmark.realtime do
  1000_000.times { string_AZ["r"] }
end

symbol_time = Benchmark.realtime do
  1000_000.times { symbol_AZ[:r] }
end

puts "String time: #{string_time} seconds."
puts "Symbol time: #{symbol_time} seconds."

Wynik to:

String time: 0.21983 seconds.
Symbol time: 0.087873 seconds.
Yurii
źródło
2
Nie traćmy z oczu faktu, że jest to jedna dziesiąta sekundy.
Casey
To wszystko jest względne. Czasami setna sprawa.
Yurii
2
Setna część sekundy w milionie iteracji? Jeśli to najlepsza dostępna optymalizacja, to myślę, że Twój program jest już całkiem dobrze zoptymalizowany.
Casey
0
  • używaj symboli jako identyfikatorów kluczy hash

    {key: "value"}

  • symbole pozwalają na wywołanie metody w innej kolejności

     def write (plik :, dane :, tryb: "ascii")
          # usunięto dla zwięzłości
     koniec
     napisz (dane: 123, plik: "test.txt")
  • zamrozić, aby zachować jako ciąg i zaoszczędzić pamięć

    label = 'My Label'.freeze

Oshan Wisumperuma
źródło