Контексты функций в Action Script

Posted by - February 20, 2009

Я люблю использовать анонимные функции, передавать функции по ссылке, объявлять функции прямо в теле другой функции и т.п. Это удобно и практично, но с этими механизмами могут возникнуть некоторые проблемы. Начиная с версии 9 Flash Player сохраняет в this функции её родителя. Звучит просто, но все ли понимают, что это значит и как тяжело было раньше без этого?

Например, теперь можно описать такую функцию:

public class TestClass
{
	var property : Number;

	function updateValue(value : Number) : void
	{
		TestClass(this).property = value;
	}
}

и передавать её куда угодно:

var func : Function = new TestClass().updateValue;
func(555);

и быть уверенным, где-бы её не вызвали в this будет экземпляр класса TestClass. Но я не об этом, есть более любопытные действия, которые можно производить над функциями в Action Script, их мы их рассмотрим.

Асинхронные вызовы

Скорее всего вы сталкивались с задачей вызова удаленного метода на сервере и обработки результата этого вызова. Допустим у нас есть класс сервиса ServerService, который принимает в конструктор ссылку на функцию, которая должна обработать ответ и мы выполняем типичную задачу обновления свойства исходного объекта:

class Example
{
	function updateItem(item : SomeObject) : void
	{
		_tempObject = item;
		new ServerService(onGetResult).getResult(item.startValue);
	}

	function onGetResult(result : Object) : void
	{
		_tempObject.endValue = result;
	}

	private var _tempObject : SomeObject;
}

Всё написано верно, но зачем так сложно? Давайте упростим подобную ерунду, «умным» кодом:

function updateItem(item : SomeObject) : void
{
	new ServerService(onGetResult).getResult(item.startValue);

	function onGetResult(result : Object) : void
	{
		item.endValue = result;
	}
}

В данном случае функция onGetResult имеет доступ ко всем переменным функции updateItem и к её аргументу item в частности. Такой прием во многих случаях может сократить объем кода и убрать негативный оттенок асинхронности. Кстати, в this функции onGetResult будет уже не экземпляр Example, а просто global.

Множественные асинхронные вызовы

Ещё интереснее ситуации когда нужно сделать несколько асинхронных запросов в цикле, а затем обработать каждый ответ соответственно, например:

function updateItems(items : ArrayCollection) : void
{
	for each (var item : SomeObject in items)
	{
		new ServerService(onGetResult).getResult(item.startValue);
	}

	function onGetResult(result : Object) : void
	{
		item.endValue = result;
	}
}

Данным кодом мы не достигнем желаемого результата. В тот момент когда сервер вернёт нам ответы, переменная item будет ссылаться на последний элемент коллекции items и все данные присвоятся только ему, слишком много чести! В таких ситуациях не помогает ни сохраняемый контекст функции ни область видимости переменных родителя, тут нужно что-то другое.

Зачастую можно воспользоваться так называемым Loader-ом:

function updateItems(items : ArrayCollection) : void
{
	for each (var item : SomeObject in items)
	{
		new ValueLoader(item);
	}
}

class ValueLoader
{
	public function ValueLoader(item : SomeObject)
	{
		new ServerService(onGetResult).getResult(item.startValue);

		function onGetResult(result : Object) : void
		{
			item.endValue = result;
		}
	}
}

Так как контекста функции недостаточно что-бы сохранить item для обновления его после ответа сервера, мы создаем над функцией обёртку — класс, которые способен запомнить в контексте всё что нужно. Так как конструктор класса всё та же функция, аргумент item без проблем будет доступен в функции onGetResult.

Стандартизированый объект ContextFunction

В конце концов, если вы нежелаете плодить массу всевозможных Loader-ов, можно ввести универсальный тип — паттерн для многократного использования:

class ContextFunction
{
	public function ContextFunction(targetFunction : Function, ... args)
	{
		_contextArgumnets = args;
		_targetFunction = targetFunction;
	}

	public function func(... args) : void
	{
		var targetArguments : Array = args.concat(_contextArgumnets);
		_targetFunction.apply(this, targetArguments);
	}

	private var _contextArgumnets : Array;

	private var _targetFunction : Function;
}

Суть решения в том, что экземпляр ContextFunction определяется ссылкой на функцию с конкретной логикой и набором неопределённых аргументов, которые получит функция, когда её кто-то вызовет. Так же, к этим аргументам добавятся ещё что-то, по желанию вызывающей сущности. Рассмотрим пример для прояснения:

function updateItems(items : ArrayCollection) : void
{
	for each (var item : SomeObject in items)
	{
		new ServerService(new ContextFunction(onGetResult, item).func).
			getResult(item.startValue);
	}
}

function onGetResult(result : Object, item : SomeObject) : void
{
	item.endValue = result;
}

Это по-сути то же решение, что и с Loader-ом, только более универсальное. Экземпляр ContextFunction сохраняет onGetResult, которая получит ответ от сервера, а также ссылку на item для которого запрашивалось серверное значение. То-есть, мы, отказываясь от контекста функции вообще, используем экземпляр вспомагательного класса, для сохранения нужных значений.

В заключение, могу вас уверить, что все эти трюки используются мной на практике очень часто и эффективно. Это не высосанные из пальца проблемы.

7 Comments on Контексты функций в Action Script

Closed

  1. Для данного класса задач более правильным является использование Command.
    Конечно это требует незначительно больше времени на создание реализации под каждую, но обеспечивает большую прозрачность кода

    • tearaway_Tea says:

      Если вы имеете ввиду паттерн Command в разрезе фреймфорка Кейнгорм, то я очень сомневаюсь, что реализация будет прозрачнее. Что в вышепреведённых примерах есть сомнительное и не поддающееся моментальному пониманию?

  2. Nox Noctis says:

    Не совсем понимаю, к чему ведет эта статья. Это повесть о том, как надо или как не надо делать? Или о том, что так вообще можно (или когда-то можно было) делать? :)

    Всего две вещи здесь от АС3: первая — в первом примере в АС2 можно было бы добиться того, чтобы this не привелся к классу, вторая — использование as в выражении (args as Array), где этот оператор вообще-то не нужен, поскольку args и так массив. Все остальное здесь от АС2 — как подходы, так и стремноватый код. Надеюсь, это все же “псевдокод” а не то, что реально предлагается использовать в АС3.

    “Множественные асинхронные вызовы” — почему, интересно, здесь “не помогает ни сохраняемый контекст функции”? Тем более что в следующей секции рассказывается, как именно контекст функции может помочь. Можно было легко понаделать делегатов и раздать им свои айтемы. В целом метод с лоадером сильно смахивает на хороший способ почесать левой пяткой правое ухо.

    “Стандартизированый объект ContextFunction” — довольно странно в феврале 2009 года видеть такой код. :) Этот прием обычно называется “делегирование”, он широко используется в ECMA-образных языках (самое массовое — Javascript и AS1-2) за счет существования такого явления как “объект активации функции”. Этот объект создается для каждой функции и содержит свой собственный нэймспэйс для локальных переменных. Этот неймспейс является дочерним по отношению к контексту объявления (за счет чего имеет доступ ко всему, что доступно из контекста вызова). Ссылку на объект активации получить нельзя (по спецификации _должно быть_ нельзя). Эту ссылку хранит только вызываемая функция и те функции, которые были созданы внутри неё (за счет того, что новые объекты активации являются дочерними). И так, зная все это, можно представить всякие-разные выкрутасы. Основной из которых — использование оболочки вроде той, которую предлагаешь ты. Такая оболочка была включена в стандартный пакет mx.utils.Delegate во Flash MX 2004. 5 лет назад. :)

    Вся необходимость в делегировании возникала от того, что для функция имел значение только контекст вызова, а не контекст объявления. То есть, относительно кого вызвали — тот и контекст. В АС3 необходимость в делегировании отпала практически начисто. Теперь этот прием уместен только там, где ты видишь, что никак “по-честному” сделать либо не получается, либо геморно и неудобно. Использовать делегирование в АС3 повсеместно — значит не понимать, самой сути того, что контекст теперь неотчуждаем от объявленного в классе метода.

    > function updateItem(item : SomeObject) : void
    > {
    > _tempObject = item;
    > new ServerService(onGetResult).getResult(item.startValue);
    > }

    Создание объекта без сохранения ссылки на него (мол, оно там само как-нибудь почему-нибудь не убьется сборщиком) — это уже само по себе то, за что полагается отрывать конечности. Это развивает смекалку того, кто в этот код полезет. Но, допустим, это мой снобизм.

    Главный вопрос — зачем предлагается вынуть метод-обработчик из контекста класса и сделать его анонимным? Это единственная цель этого мероприятия? От чего должно наступить счастье? От уменьшения объема кода?

    • tearaway_Tea says:

      Очень приятно, что вы так внимательно прочитали статью и высказали столь конструктивные замечания и критику!

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

      Использование оператора as убрал, там оно действительно не нужно.

      “Множественные асинхронные вызовы” — помоему, я доступно объяснил почему не помогает сохраняемый контекст функции. Так как цикл пересетывает переменную item несколько раз, и во время возвращений ответов от сервера, у всех функций в контексте одно и то же значение переменной item. И я как раз и предлагаю использовать несколько различных форм делегатов для решения этой задачи. Причем тут левое ухо? Предложите решение проще? (кроме посылания запросов последовательно, а не параллельно).

      Да, да, использовал когда-то делегаты в Флексе 1.5, тема не нова, ну и что? Не стоит об этом теперь писать? Повторюсь, это практическое решение проблем асинхронных вызовов в разрезе сохранения переменных в контекстах функций, которое зачастую именно не удаётся “никак “по-честному” сделать либо не получается, либо геморно и неудобно”.

      Создавать объект без сохранения ссылки в случае вызова серверного метода оправдано. Пока не отработает RemoteObject, объект сервиса никуда не денется. Я так полагаю. В чем развитие смекалки? В том что-бы понять, что вызвалось и в какой хендлер прийдёт ответ?

      Да, это цель этого мероприятия, вынуть метод из класса: в случае одного асинхронного вызова, мы избавляемся от лишней приватной переменной класса; в случае множественных вызовов, от коллекции таких переменных и какого-то соответствия между ними и вызовами сервеных методов.

  3. tearaway_Tea says:

    Да, пожалуй название статьи не соответствует её сути =)

  4. masta says:

    Функции в контексте – зло, так как это источник мемори-лагов.