Основные принципы программирования: интроспекция и рефлексия

Отредактировано

43К открытий45К показов

Рассказывает Аарон Краус 

Часто во время работы программы нам бывает нужна информация о данных — например, какой у них тип или являются ли они экземпляром класса (в ООП). Опираясь на эти знания, нам нужно проводить над данными некоторые операции, или даже изменять их — но необходимого вида данных у нас может и не быть! Если вы ничего не поняли, не расстраивайтесь — мы подробно во всём разберёмся. Всё, что я здесь описал — это иллюстрация целей двух возможностей, присутствующих почти в каждом современном языке программирования: интроспекции и рефлексии.

Интроспекция

Интроспекция — это способность программы исследовать тип или свойства объекта во время работы программы. Как мы уже упоминали, вы можете поинтересоваться, каков тип объекта, является ли он экземпляром класса. Некоторые языки даже позволяют узнать иерархию наследования объекта. Возможность интроспекции есть в таких языках, как Ruby, Java, PHP, Python, C++ и других. В целом, инстроспекция — это очень простое и очень мощное явление. Вот несколько примеров использования инстроспекции:

			// Java
 
if(obj instanceof Person){
   Person p = (Person)obj;
   p.walk();
}
		
			//PHP
 
if ($obj instanceof Person) {
   // делаем что угодно
}
		

В Python самой распространённой формой интроспекции является использование метода dir для вывода списка атрибутов объекта:

			# Python
 
class foo(object):
  def __init__(self, val):
    self.x = val
  def bar(self):
    return self.x
 
...
 
dir(foo(5))
=> ['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'bar', 'x']
		

В Ruby интроспекция очень полезна — в частности из-за того, как устроен сам язык. В нём всё является объектами — даже класс — и это приводит к интересным возможностям в плане наследования и рефлексии (об этом ниже). Если вы хотите узнать об этом больше, советую прочитать мини-цикл Metaprogramming in Ruby.

Прим. перев. Также не будет лишним прочитать нашу статью, посвящённую интроспекции в Ruby.

Вот несколько простых примеров интроспекции с использованием IRB (Interactive Ruby Shell):

			# Ruby
 
$ irb
irb(main):001:0> A=Class.new
=> A
irb(main):002:0> B=Class.new A
=> B
irb(main):003:0> a=A.new
=> #<A:0x2e44b78>
irb(main):004:0> b=B.new
=> #<B:0x2e431b0>
irb(main):005:0> a.instance_of? A
=> true
irb(main):006:0> b.instance_of? A
=> false
irb(main):007:0> b.kind_of? A
=> true
		

Вы также можете узнать у объекта, экземпляром какого класса он является, и даже “сравнить” классы.

			# Ruby
 
irb(main):008:0> A.instance_of? Class
=> true
irb(main):009:0> a.class
=> A
irb(main):010:0> a.class.class
=> Class
irb(main):011:0> A > B
=> true
irb(main):012:0> B <= A
=> true
		

Однако интроспекция — это не рефлексия; рефлексия позволяет нам использовать ключевые принципы интроспекции и делать действительно мощные вещи с нашим кодом.

Рефлексия

Интроспекция позволяет вам изучать атрибуты объекта во время выполнения программы, а рефлексия — манипулировать ими. Рефлексия — это способность компьютерной программы изучать и модифицировать свою структуру и поведение (значения, мета-данные, свойства и функции) во время выполнения. Простым языком: она позволяет вам вызывать методы объектов, создавать новые объекты, модифицировать их, даже не зная имён интерфейсов, полей, методов во время компиляции. Из-за такой природы рефлексии её труднее реализовать в статически типизированных языках, поскольку ошибки типизации возникают во время компиляции, а не исполнения программы (подробнее об этом здесь). Тем не менее, она возможна, ведь такие языки, как Java, C# и другие допускают использование как интроспекции, так и рефлексии (но не C++, он позволяет использовать лишь интроспекцию).

По той же причине рефлексию проще реализовать в интерпретируемых языках, поскольку когда функции, объекты и другие структуры данных создаются и вызываются во время работы программы, используется какая-то система распределения памяти. Интерпретируемые языки обычно предоставляют такую систему по умолчанию, а для компилируемых понадобится дополнительный компилятор и интерпретатор, который следит за корректностью рефлексии.

Мне кажется, что мы сказали много об определении рефлексии, но смысла это пока несёт мало. Давайте взглянем на примеры кода ниже (с рефлексией и без), каждый из которых создаёт объект класса Foo и вызывает метод hello.

			// ECMAScript - как JavaScript
 
// Без рефлексии
new Foo().hello()
 
// С рефлексией
 
// предполагаем, что Foo принадлежит this
new this['Foo']()['hello']()
 
// или не предполагаем
new (eval('Foo'))()['hello']()
 
// или вообще не заморачиваемся
eval('new Foo().hello()')
		
			// Java
 
// Без рефлексии
Foo foo = new Foo();
foo.hello();
 
// С рефлексией
Object foo = Class.forName("complete.classpath.and.Foo").newInstance();
// Альтернатива: Object foo = Foo.class.newInstance();
Method m = foo.getClass().getDeclaredMethod("hello", new Class<?>[0]);
m.invoke(foo);
		
			# Python
 
# Без рефлексии
obj = Foo()
obj.hello()
 
# С рефлексией
class_name = "Foo"
method = "hello"
obj = globals()[class_name]()
getattr(obj, method)()
 
# С eval
eval("Foo().hello()")
		
			# Ruby
 
# Без рефлексии
obj = Foo.new
obj.hello
 
# С рефлексией
class_name = "Foo"
method = :hello
obj = Kernel.const_get(class_name).new
obj.send method
 
# С eval
eval "Foo.new.hello"
		

Этот список отнюдь не исчерпывает возможности рефлексии. Это очень мощный принцип, который к тому же является обычной практикой в метапрограммировании. Тем не менее, при использовании рефлексии нужно быть очень внимательным. Хотя у неё и есть свои преимущества, код, использующий рефлексию, значительно менее читаем, он затрудняет отладку, а также открывает двери по-настоящему плохим вещами, например, инъекции кода через выражения eval.

Eval-выражения

Некоторые рефлективные языки предоставляют возможность использования eval-выражений — выражений, которые распознают значение (обычно строку) как выражение. Такие утверждения — это самый мощный принцип рефлексии и даже метапрограммирования, но также и самый опасный, поскольку они представляют собой угрозу безопасности.

Рассмотрим следующий пример кода на Python, который принимает данные из стороннего источника в Сети (это одна из причин, по которой люди пользуются eval-выражениями):

			session['authenticated'] = False
data = get_data()
foo = eval(data)
		

Защита программы будет нарушена, если кто-то передаст в метод get_data() такую строку:

			"session.update(authenticated=True)"
		

Для безопасного использования eval-утверждений нужно сильно ограничивать формат входных данных — и обычно это лишь занимает лишнее время.

Заключение

Интроспекция и рефлексия — это очень мощные инструменты современных языков, и их понимание может позволить вам писать по-настоящему крутой код. Ещё раз отметим: интроспекция — это изучение атрибутов объекта, а рефлексия — это манипуляция ими. Будьте внимательны при использовании рефлексии, поскольку она может сделать ваш код нечитаемым и уязвимым. Чем больше сила, тем больше и ответственность — вот девиз всего, что связано с метапрограммированием.

Следите за новыми постами
Следите за новыми постами по любимым темам
43К открытий45К показов