Использование 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), за исключением:

  • внутри вложенной (не асинхронной) анонимной функции,

  • внутри блока lock,

  • в анонимной функции, использующейся для дерева выражений,

  • в небезопасном (unsafe) контексте.

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

  • тип имеет доступный метод экземпляра (не статичный метод) или метод расширение GetAwaiter() без параметров, без параметров типа (не generic, GetAwaiter<T>() не разрешен) и возвращающий тип A,

  • или объект имеет тип dynamic.

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

  • реализует интерфейс System.Runtime.CompilerServices.INotifyCompletion,

  • имеет доступное для чтения свойство IsCompleted с типом bool,

  • имеет доступный метод экземпляра (не static) GetResult() без параметров и без параметров типа (не generic), на возвращаемый тип ограничений нет.

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

  • GetAwaiter() нужен для получения объекта ожидания (awaiter) выполнения операции. Тип А это тип объекта ожидания.

  • свойство IsCompleted необходимо для определения завершения операции, если операция завершена, то код будет выполняться синхронно.

  • свойство INotifyCompletion.OnCompleted используется для подписки на завершение операции.

  • метод GetResult() для получения результата выполнения операции. Это может быть успешное завершение, со значением результата, если требуется, или исключение, которое выбрасывается методом GetResult().

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

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

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");
		}
	}
}

Ссылки

Комментарии (1)


  1. BenjaminMoore
    14.11.2022 13:59
    +2

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