Adobe

индекс
116,75

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

Я люблю использовать анонимные функции, передавать функции по ссылке, объявлять функции прямо в теле другой функции и т.п. Это удобно и практично, но с этими механизмами могут возникнуть некоторые проблемы. Начиная с версии 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 для которого запрашивалось серверное значение. То-есть, мы, отказываясь от контекста функции вообще, используем экземпляр вспомагательного класса, для сохранения нужных значений.

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

комментарии (42)

+1
pixelcube #
Кстати, в this функции onGetResult будет уже не экземпляр Example, а просто global.
Не совсем :) там будет не global, а некая обертка объектов текущего *.as файла (именно файла, а не пакета, и не global), она находится чуть ниже в scope-очереди, однако позволяет достучаться вплоть до global.
0
tearaway_Tea #
Да, повидимому так и есть. Я имел ввиду, что будет контекст не текущего родителя (класса, функции), а что-то более глобальное =)
0
pixelcube #
Стоит отметить, что с усложненеим кода в onGetResult, значительно усложнится и поиск багов.
И надо иметь хорошие знания и опыт в цепочке видимости (scope-chain) и постоянно понимать что onGetResult — это вовсе не метод а функциональная кложура (не знаю как перевести closure, но слово «кожура» — кстати, подходит :)
В командной разработке, думаю, лучше не прибегать к таким методам, ато у новичков от постоянных багов сорвет башню.

0
tearaway_Tea #
Ну, в общем-то, не обязательно помещать много кода в onGetResult, думаю правильным решением будет делегирование обработки вспомогательному классу или просто продиспатить ивент.

На практике, даже в очень больших и запутанных бизнес-логикой проектах, подобные решения как раз таки упрощали код и его понимание. Как минимум на предмет отстутствия бесконечных временных приватных переменных классов для сохранения обрабатываемых объектов.
0
Snut #
Руки бы отрывал за вложенные функции.
+2
sse #
А вы на них смотрите как на именованные замыкания — и полезно, и руки отрывать не надо :))
–2
Snut #
Класс! Улыбнулся.
0
tearaway_Tea #
Объясните мне, почему?
–1
Snut #
Да потомучто нереально их дебажить это раз, и предсказать их поведение — тоже
–1
tearaway_Tea #
Ну, может быть ваша ІDE сталкивается с проблемами при отладке, в Flex Builder всё великолепно. А вот для того, чтобы поведение предсказывалось, я и написал данную статью. Надеюсь, хоть чем-то она кому-то поможет.
0
Snut #
как ни странно работаю во Flex IDE

Такой не скромный вопрос, вы когда-нибудь работали в команде хотя бы 5+ человек?
–1
tearaway_Tea #
Я тим-лид команды флексеров (4 человека) =)
Ранее всегда работал в командах от 3-4 человек. Я понимаю, что вы стандартно клоните, к тому, что такой код тяжело сопровождать новичкам на проектах — это абсурд. В вышеприведённом коде нет особо секретных костылей или дёрти-хаков, он прост как визуально так и семантически.
+1
Snut #
эхххх флексеры… не буду ничего говорить. вы бы еще хендлеры запихивали как вложенные функции и тогда бы проекты бы после открытия сжирали по 300 метров… все молчу.
–2
tearaway_Tea #
Кстати, да, засовываем довольно успешно. Пользуемся слабыми ссылками при добавлении подобных хендлеров, в результате сборщик мусора отлично их подчищает.
+2
Snut #
Мое ИМХО — Слабые ссылки зло, сборщик мусора нам не подконтролен, если подписался сам же и отпишись. Я так делаю и никому этот путь не навязываю. Спор беспочвенен.
0
tearaway_Tea #
Слабые ссылки при добавлении ивент хендлеров это дополнительный инструмент, который предоставила Адобе. Правильно пользуясь ими, можно сэкономить в коде и не добавлять в стройную архитектуру проекта вынужденое «уничтожение» для некоторых объектов.

Да и ещё раз да, листенеры нужно удалять вручную, причём, желательно, в тех же местах где они были добавлены. Но, бывают ситуации, когда достаточно расчитывать на слабые ссылки. Если желаете могу приподнести простой пример.
0
Snut #
Да, было бы неплохо увидеть пример где применение слабых ссылок:
1) реально обоснованно
2) невозможно использовать стандартный метод подписки, отписки.
0
tearaway_Tea #
0
Snut #
Safari can’t connect to the server.
+1
Gaen #
Ваш код заставляет мои глаза кровоточить. Если уж перешли на AS3, то тогда забудьте про AS2 и включите strict mode раз и навсегда.
–1
tearaway_Tea #
Не очевидно, почему нужно забыть AS2, только потому, что версия 3 счастливое число?
Режим strict mode включён всегда, надеюсь у вас тоже.
0
Gaen #
Прошу прощения что так сразу накинулся, попробую объяснить.
Возьму пример из начала текста.
Например, теперь можно описать такую функцию:
public class TestClass
{
	var property : Number;

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


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

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

Ещё пример:
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;
}


Далеко не сразу я нашёл объявление _tempObject. Когда я в первый раз читал статью, я решил что оно вообще пропущено, и думаю, не я один. Учитывая тему, к которой относится пример, сразу становится неочевидно, к какой области видимости относится данная переменная, что вносит большую путаницу. Просто потому что переменные и константы всегда определяются до описания методов. Хотя тут я скорее всего придираюсь к мелочам.

Надеюсь, вы воспримете это как конструктивную критику, или даже убедите меня в моей неправоте. Ибо споры между уважающими друг друга оппонентами — процесс довольно приятный и познавательный :)
0
tearaway_Tea #
По-поводу TestClass, зачем так конструктор? Он не нужен, в примере использования ничего в класс не передаётся:

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


Да, в первом примере можно использовать статический метод, а можно вообще ничего не использовать — пример фиктивен. Это просто предисловие к основной теме про контексты, не стоит его воспринимать буквально.

По-поводу _tempObject, даже не знаю как прокоментировать, это не то что-бы придирание к мелочам, это какое-то тонкое издевательство, надеюсь вы шутили =).

0
Gaen #
По поводу _tempObject я пожалуй погорячился, будем считать что я шутил :)

Про конструктор я говорил относительно этой строчки:

TestClass(this).property = value;
0
tearaway_Tea #
Это:

TestClass(this)

операция приведения типов, сдесь она не обязательна, я написал для наглядности.
Не следует путать запись выше с:

new TestClass(this)
0
Gaen #
Публично признаю себя слепым идиотом, не сумевшим отличить кастинг от создания экземпляра. Хотя осознание того, что это кастинг, делает эту строчку ещё более загадочной =)
Какую логику вы в неё закладываете?
0
tearaway_Tea #
Что-бы подчеркнуть, что this это екземпляр класса TestClass, а не что-либо другое. В противном случае будет ошибка времени исполнения.
0
Gaen #
Тогда мне кажется лучше использовать this as TestClass, т.к. эти касты вроде отличаются. Пойду погуглю.
0
tearaway_Tea #
Не лучше, оператор as — это так же оператор приведения типа, но тихий. Если приводимый объект, будет другого типа, то оператор вернёт null и эксепшена не будет. В этом основная разница.
0
Gaen #
Имхо кастинг в данном случае — это уже паранойя, классы должны быть чёрными ящиками, но сами себе-то они могут доверять! В конце концов это вы создаёте код, и совершенно сознательно пишете данный метод в этом классе. Надо больше в себя верить! :)
0
tearaway_Tea #
Несомненно! Но, так как речь идёт о сохраняемом контексте функций, то данная запись подчёркивает что this(контекст) будет именно таким как и ожидается. В AS2 например, этот же код выполнился бы с эксепшеном, так как this не был бы экземпляром класса TestClass, но того класса, в котором функция бы вызывалась.

Это и есть суть примера, который не стоит принимать как пример для написания кода, только для понимания.
0
Gaen #
/*
В as2 эта строчка бы выкинула эксепшен, так как метод исполнился бы не в контексте нашего класса, а в том контексте, откуда он был вызван.
*/

Один коммент мог заменить всю нашу беседу :)
0
tearaway_Tea #
Надеюсь, беседа будем полезна как вам, так и тем, кто возможно её прочитает.
0
Gaen #
Несомненно, уже нашлись заинтересованные:
flasher.ru/forum/showthread.php?t=122122
0
tearaway_Tea #
Интересные у вас там ребята. Больше мне сказать нечего.
0
DarkLight #
ого, в AS2 есть exception-ы на null object reference-ы:) почему все остальные флешеры этого не знали?:)
0
tearaway_Tea #
ОК, вы правы, эксепшенов до 9 версии флеш плеера, по-моему, небыло. Но, даже не учитывая это, суть моих мыслей надеюсь вам понятна.
0
Garret #
Каково еще применение вложенных функций кроме описанного?
Про хендлеры было упоминание выше, я согласен, что полагаться на сборщик не стоит, да и дебажить такие конструкции не слишком удобно.
Кроме того, лично для меня польза того, что обработчик находится в вызывающим асинхронный запрос методе (а по сути в одной области видимости) не совсем очевида. Буду рад если объясните.
0
tearaway_Tea #
Предпочитаю не полагаться на сборщик, а контролировать его посекундно. Для этого мы имеем неплохой инструмент от Адоби, называется Flex Builder Profiler. В нём можно видеть, сколько объектов или функций не уничтожилось, сколько памяти израсходовано. Зачем гадать? Можно же проверить! =)

В чём неудобство отладки таких конструкций? Брейкпоинты в них обрабатываются Flex Builder-ом, стек трейс и состояние памяти показается. Где неудобства?

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

Есть ещё несколько вариантов использования вложеных функций, например передавая их в методы массивов forEach(), filter() — им будут доступны все переменные родительской функции и это можно использовать для поэлементной обработки.
0
pixelcube #
Судя по опыту, в отношении GC — не следует так сильно полагаться. Так в профайлере (debug-версия плеера) и продакшн-версии (обычный плеер) — могут быть совершенно различные картины.
0
tearaway_Tea #
Хм, я про это не знал. Но надеюсь, что это было давно и уже не актуально. Версии плееров постоянно обновляются, не думаю, что Адоби оставила бы таку дыру в оптимизации приложений под её плеер.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.