войти зарегистрироваться

Adobe Flex whois

индекс
62,91

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

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

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

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

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

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

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

                      Да и ещё раз да, листенеры нужно удалять вручную, причём, желательно, в тех же местах где они были добавлены. Но, бывают ситуации, когда достаточно расчитывать на слабые ссылки. Если желаете могу приподнести простой пример.
                      • Да, было бы неплохо увидеть пример где применение слабых ссылок:
                        1) реально обоснованно
                        2) невозможно использовать стандартный метод подписки, отписки.
  • Ваш код заставляет мои глаза кровоточить. Если уж перешли на AS3, то тогда забудьте про AS2 и включите strict mode раз и навсегда.
    • Не очевидно, почему нужно забыть AS2, только потому, что версия 3 счастливое число?
      Режим strict mode включён всегда, надеюсь у вас тоже.
      • Прошу прощения что так сразу накинулся, попробую объяснить.
        Возьму пример из начала текста.
        Например, теперь можно описать такую функцию:
        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. Когда я в первый раз читал статью, я решил что оно вообще пропущено, и думаю, не я один. Учитывая тему, к которой относится пример, сразу становится неочевидно, к какой области видимости относится данная переменная, что вносит большую путаницу. Просто потому что переменные и константы всегда определяются до описания методов. Хотя тут я скорее всего придираюсь к мелочам.

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

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


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

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

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

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

            TestClass(this).property = value;
            • Это:

              TestClass(this)

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

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

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

                          Один коммент мог заменить всю нашу беседу :)
                          • Надеюсь, беседа будем полезна как вам, так и тем, кто возможно её прочитает.
                        • ого, в AS2 есть exception-ы на null object reference-ы:) почему все остальные флешеры этого не знали?:)
                          • ОК, вы правы, эксепшенов до 9 версии флеш плеера, по-моему, небыло. Но, даже не учитывая это, суть моих мыслей надеюсь вам понятна.
  • Каково еще применение вложенных функций кроме описанного?
    Про хендлеры было упоминание выше, я согласен, что полагаться на сборщик не стоит, да и дебажить такие конструкции не слишком удобно.
    Кроме того, лично для меня польза того, что обработчик находится в вызывающим асинхронный запрос методе (а по сути в одной области видимости) не совсем очевида. Буду рад если объясните.
    • Вот вам литература, читайте на здоровье:
      livedocs.adobe.com/flex/3/html/help.html?content=03_Language_and_Syntax_21.html
      www.mikechambers.com/blog/2008/10/08/function-closures-and-this-in-actionscript-3/
      www.transcendentaxis.com/dthompson/blog/archives/19#more-19
    • Предпочитаю не полагаться на сборщик, а контролировать его посекундно. Для этого мы имеем неплохой инструмент от Адоби, называется Flex Builder Profiler. В нём можно видеть, сколько объектов или функций не уничтожилось, сколько памяти израсходовано. Зачем гадать? Можно же проверить! =)

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

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

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