Я люблю использовать анонимные функции, передавать функции по ссылке, объявлять функции прямо в теле другой функции и т.п. Это удобно и практично, но с этими механизмами могут возникнуть некоторые проблемы. Начиная с версии 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 для которого запрашивалось серверное значение. То-есть, мы, отказываясь от контекста функции вообще, используем экземпляр вспомагательного класса, для сохранения нужных значений.
В заключение, могу вас уверить, что все эти трюки используются мной на практике очень часто и эффективно. Это не высосанные из пальца проблемы.
Для данного класса задач более правильным является использование Command.
Конечно это требует незначительно больше времени на создание реализации под каждую, но обеспечивает большую прозрачность кода
Если вы имеете ввиду паттерн Command в разрезе фреймфорка Кейнгорм, то я очень сомневаюсь, что реализация будет прозрачнее. Что в вышепреведённых примерах есть сомнительное и не поддающееся моментальному пониманию?
Не совсем понимаю, к чему ведет эта статья. Это повесть о том, как надо или как не надо делать? Или о том, что так вообще можно (или когда-то можно было) делать? :)
Всего две вещи здесь от АС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);
> }
Создание объекта без сохранения ссылки на него (мол, оно там само как-нибудь почему-нибудь не убьется сборщиком) — это уже само по себе то, за что полагается отрывать конечности. Это развивает смекалку того, кто в этот код полезет. Но, допустим, это мой снобизм.
Главный вопрос — зачем предлагается вынуть метод-обработчик из контекста класса и сделать его анонимным? Это единственная цель этого мероприятия? От чего должно наступить счастье? От уменьшения объема кода?
Очень приятно, что вы так внимательно прочитали статью и высказали столь конструктивные замечания и критику!
Первый абзац статьи о сохраняемом this в функции был как бы вступительным, и в общем-то не имеет особого значения для остальных примеров, тут вы правы, но я это отметил так же. Я его добавил как эпиграф к статье о контекстах =).
Использование оператора as убрал, там оно действительно не нужно.
“Множественные асинхронные вызовы” — помоему, я доступно объяснил почему не помогает сохраняемый контекст функции. Так как цикл пересетывает переменную item несколько раз, и во время возвращений ответов от сервера, у всех функций в контексте одно и то же значение переменной item. И я как раз и предлагаю использовать несколько различных форм делегатов для решения этой задачи. Причем тут левое ухо? Предложите решение проще? (кроме посылания запросов последовательно, а не параллельно).
Да, да, использовал когда-то делегаты в Флексе 1.5, тема не нова, ну и что? Не стоит об этом теперь писать? Повторюсь, это практическое решение проблем асинхронных вызовов в разрезе сохранения переменных в контекстах функций, которое зачастую именно не удаётся “никак “по-честному” сделать либо не получается, либо геморно и неудобно”.
Создавать объект без сохранения ссылки в случае вызова серверного метода оправдано. Пока не отработает RemoteObject, объект сервиса никуда не денется. Я так полагаю. В чем развитие смекалки? В том что-бы понять, что вызвалось и в какой хендлер прийдёт ответ?
Да, это цель этого мероприятия, вынуть метод из класса: в случае одного асинхронного вызова, мы избавляемся от лишней приватной переменной класса; в случае множественных вызовов, от коллекции таких переменных и какого-то соответствия между ними и вызовами сервеных методов.
Да, пожалуй название статьи не соответствует её сути =)
Функции в контексте – зло, так как это источник мемори-лагов.
Вы проверяли в профайлере?