C# жду продолжения: не совсем то же самое?
Прочитав ответ Эрика Липперта, у меня сложилось впечатление, что await
а также call/cc
в значительной степени две стороны одной медали, с большинством синтаксических различий. Тем не менее, при попытке реально реализовать call/cc
в C# 5 я столкнулся с проблемой: либо я неправильно понимаю call/cc (что вполне возможно), либо await только напоминает call/cc.
Рассмотрим псевдокод так:
function main:
foo();
print "Done"
function foo:
var result = call/cc(bar);
print "Result: " + result;
function bar(continuation):
print "Before"
continuation("stuff");
print "After"
Если я правильно понимаю call / cc, то это должно вывести:
Before
Result: stuff
Done
Важно, что при вызове продолжения состояние программы восстанавливается вместе с историей вызовов, так что foo
возвращается в main
и никогда не возвращается bar
,
Однако, если реализовано с использованием await
в C# вызов продолжения не восстанавливает эту историю вызовов. foo
возвращается в bar
и нет никакого способа (что я вижу), что await
может использоваться, чтобы сделать правильную историю вызовов частью продолжения.
Пожалуйста, объясните: я полностью неправильно понял работу call/cc
или await
просто не совсем как call/cc
?
Теперь, когда я знаю ответ, я должен сказать, что есть веская причина считать их довольно похожими. Посмотрите, как выглядит вышеприведенная программа в псевдо-C#-5:
function main:
foo();
print "Done"
async function foo:
var result = await(bar);
print "Result: " + result;
async function bar():
print "Before"
return "stuff";
print "After"
Поэтому, хотя стиль C# 5 никогда не дает нам объект продолжения для передачи значения, в целом сходство весьма поразительно. За исключением того, что на этот раз совершенно очевидно, что "After" никогда не вызывается, в отличие от примера true-call/cc, что является еще одной причиной любить C# и хвалить его дизайн!
1 ответ
await
действительно не совсем так же, как call/cc
,
Вид абсолютно фундаментальный call/cc
то, о чем вы думаете, действительно должно было бы сохранить и восстановить весь стек вызовов. Но await
это просто преобразование во время компиляции. Он делает нечто подобное, но не использует реальный стек вызовов.
Представьте, что у вас есть асинхронная функция, содержащая выражение await:
async Task<int> GetInt()
{
var intermediate = await DoSomething();
return calculation(intermediate);
}
Теперь представьте, что функция, которую вы вызываете через await
сам содержит await
выражение:
async Task<int> DoSomething()
{
var important = await DoSomethingImportant();
return un(important);
}
Теперь подумайте о том, что происходит, когда DoSomethingImportant()
заканчивается и его результат доступен. Контроль возвращается к DoSomething()
, затем DoSomething()
заканчивается и что происходит потом? Контроль возвращается к GetInt()
, Поведение точно так же , как если бы GetInt()
были в стеке вызовов. Но это не совсем так; ты должен использовать await
при каждом вызове, который вы хотите смоделировать таким образом. Таким образом, стек вызовов поднимается в стек мета-вызовов, который реализован в ожидании.
То же самое, кстати, верно и для yield return
:
IEnumerable<int> GetInts()
{
foreach (var str in GetStrings())
yield return computation(str);
}
IEnumerable<string> GetStrings()
{
foreach (var stuff in GetStuffs())
yield return computation(stuff);
}
Теперь, если я позвоню GetInts()
что я получаю обратно это объект, который инкапсулирует текущее состояние выполнения GetInts()
(так что зовет MoveNext()
на нем возобновляется работа с того места, где он остановился). Этот объект сам содержит итератор, который перебирает GetStrings()
и звонки MoveNext()
на этом. Таким образом, реальный стек вызовов заменяется иерархией объектов, которые каждый раз воссоздают правильный стек вызовов посредством серии вызовов MoveNext()
на следующем внутреннем объекте.