Практическое применение async/await в Unity

Использование async/await позволяет сделать код легче для понимания, убирает необходимость в колбеках (функций обратного вызова) и протаскивании через них необходимых данных (или их сохранения в полях объекта).

Сравнение async/await с колбеками

Пример сохранения данных в указанный слот с пользовательским комментарием.

С использованием колбеков:

public void Save(int slotIndex)
{
	if (SaveExists(slotIndex))
	{
		popupConfirm.Open("Перезаписать файл?", () => SaveCommentsRequest(slotIndex));
	}
	else
	{
		SaveCommentsRequest(slotIndex);
	}
}

void SaveCommentsRequest(int slotIndex)
{
	popupInput.Open("Укажите комментарий", comments => SaveWithComments(slotIndex, comments));
}

void SaveWithComments(int slotIndex, string comments)
{
	// сохраняем здесь
}

С использованием async/await:

async public void Save(int slotIndex)
{
	if (SaveExists(slotIndex) && !await popupConfirm.Open("Перезаписать файл?"))
	{
		return;
	}

	var comments = await popupInput.Open("Укажите комментарий");

	// сохраняем здесь
}

Требования и ограничения для реализации

Использовать await можно только внутри асинхронной функции (с модификатором async), за исключением:

await можно использовать для типов, соответствующим условиям:

Требования для типа A:

Для чего они нужны:

Пример реализации

Сперва необходим интерфейс для ожидаемого типа:

using System;

public interface IAwaitable<TResult>
{
	event Action<TResult> OnComplete;

	public Awaiter<TResult> GetAwaiter();
}

И класс для объекта ожидания:

using System;
using System.Runtime.CompilerServices;

public class Awaiter<TResult> : INotifyCompletion
{
	TResult result;

	Action continuation;

	public bool IsCompleted
	{
		get;
		private set;
	}

	public Awaiter(IAwaitable<TResult> awaitable)
	{
		void setResult(TResult result)
		{
			// отписываемся после получения результата
			awaitable.OnComplete -= setResult;

			// сохраняем результат
			IsCompleted = true;
			this.result = result;

			var c = continuation;
			// поскольку оповещаем однократно, то можно очистить
			continuation = null;
			// оповещаем о завершении операции
			c?.Invoke();
		}

		// подписываемся на результат операции
		awaitable.OnComplete += setResult;
	}

	public void OnCompleted(Action continuation)
	{
		if (IsCompleted)
		{
			continuation();
			return;
		}

		// сохраняем делегат для оповещения о завершении
		this.continuation += continuation;
	}

	public TResult GetResult()
	{
		return result;
	}
}

Если результата не нужен, то <TResult> убирается вместе с сохранением результата и GetResult() становится пустым методом, возвращающим void.

Пример окна для подверждения, реализующего IAwaitable<bool>:

using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class PopupConfirm : MonoBehaviour, IAwaitable<bool>
{
	public event Action<bool> OnComplete
	{
		add => onComplete += value;
		remove => onComplete -= value;
	}
	
	public void Confirm() => Complete(true);

	public void Cancel() => Complete(false);

	void Complete(bool result)
	{
		gameObject.SetActive(false);

		// оповещаем о результате
		onComplete?.Invoke(result);
	}

	public PopupConfirm Open(string message = null)
	{
		Message.text = message;
		gameObject.SetActive(true);

		// возвращаем этот же объект, чтобы сразу использовать await
		return this;
	}

	public Awaiter<bool> GetAwaiter() => new Awaiter<bool>(this);

	[SerializeField]
	protected TextMeshProUGUI Message;

	[SerializeField]
	protected Button ButtonOk;

	[SerializeField]
	protected Button ButtonCancel;

	event Action<bool> onComplete;

	protected virtual void Start()
	{
		ButtonOk.onClick.AddListener(Confirm);
		ButtonCancel.onClick.AddListener(Cancel);
	}

	protected virtual void OnDestroy()
	{
		ButtonOk.onClick.RemoveListener(Confirm);
		ButtonCancel.onClick.RemoveListener(Cancel);

		// при удалении оповещаем об отмене
		Cancel();
	}
}

Тестируем:

using UnityEngine;

public class TestConfirm : MonoBehaviour
{
	public PopupConfirm Confirm;

	async public void Test()
	{
		var quit = await Confirm.Open("Выйти?");
		if (quit)
		{
			Debug.Log("quit");
			Application.Quit();
		}
		else
		{
			Debug.Log("cancel");
		}
	}
}

Ссылки

@ilih
14.11.2022 15:24 UTC
Первоисточник

Комментарии

@BenjaminMoore
14.11.2022 10:59 UTC
+2

Я бы еще добавил, что при работе с async/await всегда под рукой стоит иметь UniTask
Хорошо, что описана реализация кастомного авейтера, это тоже бывает полезным