Istnieje wiele „schludnych” rzeczy, które można zrobić w dynamicznych językach, które można schować w częściach kodu, które nie są od razu oczywiste dla innego programisty lub audytora co do funkcjonalności danego fragmentu kodu.
Rozważ tę sekwencję w irb (interaktywna powłoka rubinowa):
irb(main):001:0> "bar".foo
NoMethodError: undefined method `foo' for "bar":String
from (irb):1
from /usr/bin/irb:12:in `<main>'
irb(main):002:0> class String
irb(main):003:1> def foo
irb(main):004:2> "foobar!"
irb(main):005:2> end
irb(main):006:1> end
=> nil
irb(main):007:0> "bar".foo
=> "foobar!"
To, co się tam zdarzyło, próbowałem wywołać metodę foo
w stałej String. To się nie udało. Następnie otworzyłem klasę String i zdefiniowałem metodę foo
return "foobar!"
, a następnie ją wywołałem. To zadziałało.
Jest to znane jako klasa otwarta i daje mi koszmary za każdym razem, gdy myślę o pisaniu kodu w języku Ruby, który ma jakiekolwiek zabezpieczenia lub integralność. Pewnie, że pozwala ci robić porządne rzeczy dość szybko ... ale mógłbym to zrobić, aby za każdym razem, gdy ktoś przechowywał ciąg, zapisywał go w pliku lub wysyłał przez sieć. A ta odrobina przedefiniowania ciągu może być ukryta w dowolnym miejscu kodu.
Wiele innych dynamicznych języków ma podobne rzeczy do zrobienia. Perl ma Tie :: Scalar, który może za kulisami zmienić sposób działania danego skalara (jest to nieco bardziej oczywiste i wymaga określonego polecenia, które można zobaczyć, ale skalar, który jest przekazywany z innego miejsca, może stanowić problem). Jeśli masz dostęp do Perl Cookbook, poszukaj przepisu 13.15 - Tworzenie magicznych zmiennych za pomocą remisu.
Z powodu tych rzeczy (i innych często będących częścią dynamicznych języków) wiele metod statycznej analizy bezpieczeństwa w kodzie nie działa. Perl and Undecidability pokazuje, że tak jest i wskazuje nawet na tak trywialne problemy z podświetlaniem składni ( whatever / 25 ; # / ; die "this dies!";
stanowi wyzwanie, ponieważ whatever
można je zdefiniować tak, aby pobierało argumenty lub nie w czasie wykonywania całkowicie pokonało wyróżniacz składni lub analizator statyczny).
To może stać się jeszcze bardziej interesujące w Ruby dzięki możliwości dostępu do środowiska, w którym zdefiniowano zamknięcie (patrz YouTube: Utrzymanie Ruby Rozsądne z RubyConf 2011 przez Joshua Ballanco). Dowiedziałem się o tym filmie od komentarza Ars Technica autorstwa MouseTheLuckyDog .
Rozważ następujący kod:
def mal(&block)
puts ">:)"
block.call
t = block.binding.eval('(self.methods - Object.methods).sample')
block.binding.eval <<-END
def #{t.to_s}
raise 'MWHWAHAW!'
end
END
end
class Foo
def bar
puts "bar"
end
def qux
mal do
puts "qux"
end
end
end
f = Foo.new
f.bar
f.qux
f.bar
f.qux
Ten kod jest w pełni widoczny, ale mal
metoda może być gdzie indziej ... a przy otwartych klasach, oczywiście, mogłaby zostać zdefiniowana gdzie indziej.
Uruchamianie tego kodu:
~ / $ ruby foo.rb
bar
> :)
qux
bar
b.rb: 20: in `qux ': MWHWAHAW! (RuntimeError)
from b.rb: 30: in ''
~ / $ ruby foo.rb
bar
> :)
qux
b.rb: 20: in `bar ': MWHWAHAW! (RuntimeError)
from b.rb: 29: in ''
W tym kodzie zamknięcie mogło uzyskać dostęp do wszystkich metod i innych powiązań zdefiniowanych w klasie w tym zakresie. Wybrał metodę losową i przedefiniował ją, aby zgłosić wyjątek. (zobacz klasę Binding w Ruby, aby dowiedzieć się, do czego ten obiekt ma dostęp)
Wszystkie zmienne, metody, wartość self i ewentualnie blok iteratora, do których można uzyskać dostęp w tym kontekście, są zachowane.
Krótsza wersja, która pokazuje redefinicję zmiennej:
def mal(&block)
block.call
block.binding.eval('a = 43')
end
a = 42
puts a
mal do
puts 1
end
puts a
Które, po uruchomieniu, produkuje:
42
1
43
To więcej niż wspomniana powyżej klasa otwarta, która uniemożliwia analizę statyczną. Powyżej pokazano, że zamknięcie, które jest przekazywane gdzie indziej, niesie ze sobą pełne środowisko, w którym zostało zdefiniowane. Jest to znane jako środowisko pierwszej klasy (tak jak kiedy można przekazywać funkcje, są to funkcje pierwszej klasy, to środowisko i wszystkie dostępne w tym czasie powiązania). Można było ponownie zdefiniować dowolną zmienną zdefiniowaną w zakresie zamknięcia.
Dobry czy zły, narzekający na ruby lub nie (są zastosowania, w których można chcieć dostać się do środowiska metody (patrz Bezpieczne w Perlu)), pytanie „dlaczego miałby ograniczać ruby dla projektu rządowego „tak naprawdę odpowiedź znajduje się w powyższym filmie wideo.
Jeśli się uwzględni:
- Ruby pozwala wyodrębnić środowisko z dowolnego zamknięcia
- Ruby przechwytuje wszystkie wiązania w zakresie zamknięcia
- Ruby zachowuje wszystkie wiązania jako aktywne i zmienne
- Ruby ma nowe wiązania cień starych powiązań (zamiast klonowania środowiska lub zabrania ponownego wiązania)
Biorąc pod uwagę konsekwencje tych czterech wyborów projektowych, nie można wiedzieć, co robi każdy fragment kodu.
Więcej na ten temat można przeczytać na blogu Abstract Heresies . Ten szczególny post dotyczy Schematu, w którym odbyła się taka debata. (powiązane z SO: Dlaczego Schemat nie obsługuje środowisk pierwszej klasy? )
Z czasem jednak zdałem sobie sprawę, że w środowiskach pierwszej klasy było więcej trudności i mniej mocy, niż początkowo sądziłem. W tym momencie uważam, że pierwszorzędne środowiska są w najlepszym wypadku bezużyteczne, aw najgorszym niebezpieczne.
Mam nadzieję, że ten rozdział pokazuje niebezpieczny aspekt środowisk pierwszej klasy i dlaczego zostanie poproszony o usunięcie Ruby z dostarczonego rozwiązania. Nie chodzi tylko o to, że Ruby jest językiem dynamicznym (jak wspomniano w innym projekcie, inne języki dynamiczne były dozwolone w innych projektach), ale że istnieją specyficzne problemy, które sprawiają, że niektóre języki dynamiczne są jeszcze trudniejsze do uzasadnienia.