Шаблон Async Try (blah)
Я ищу рекомендации относительно того, как обращаться со следующей ситуацией.
Я создаю методы для попыток получить некоторые данные, следуя этому шаблону:
// Typical pattern
public bool TryBlah(string key, out object value)
{
// ... set value and return boolean
}
У меня возникла проблема при попытке следовать этому шаблону в асинхронных версиях, потому что вы не можете использовать out
для методов async:
// Ideal async pattern (not allowed to use an 'out' parameter, so this fails)
public async Task<bool> TryBlah(string key, out object value)
{
// ... set value, perform some slow io operation, return bool
}
Обходным путем является возврат кортежа, содержащего ваши данные. Это работает для методов, которые возвращают один тип данных, например:
// Tuple version
public async Task<Tuple<bool, object>> TryBlah(string key)
{
// ... perform some slow io, return new Tuple<bool, object>(...)
}
Проблема заключается в том, когда вы хотите вернуть разные типы данных. Без использования async вы можете создать несколько методов с почти одинаковыми сигнатурами:
public bool TryBlah(string key, out byte[] value)
{
// ...
}
public bool TryBlah(string key, out string value)
{
// ...
}
Это здорово. Это то, что я ищу. Этот api очень прост и удобен в работе (имена методов все одинаковы, только данные, которые передаются в изменениях).
Невозможность использовать out
с помощью методов async все равно испортит это.
Один из способов обойти это - вернуть Tuple
ваших данных. Однако теперь вы не можете иметь почти идентичные сигнатуры методов, например:
// The suck... the signatures match, but you want to return different values.
// You can't do this:
public async Task<Tuple<bool, byte[]>> TryBlah(string key)
{
// ...
}
public async Task<Tuple<bool, string>> TryBlah(string key)
{
// ...
}
Эти методы терпят неудачу, потому что они имеют одинаковые подписи. Единственный способ обойти это, что приходит на ум, - дать каждому методу отличное имя, например:
public async Task<Tuple<bool, byte[]>> TryBlahByteArray(string key)
{
// ...
}
public async Task<Tuple<bool, string>> TryBlahString(string key)
{
// ...
}
Моя проблема в том, что теперь это создает то, что я считаю неприятным api, где у вас теперь есть много разных методов. Да, это не так уж и важно, но я считаю, что должен быть лучший способ.
Существуют ли другие шаблоны, которые поддаются лучшему api при работе с такими асинхронными методами? Я открыт для любых предложений.
Ответы
Ответ 1
Возможно, вы могли бы использовать Action<T>
как замену out param
Пример:
public async Task<bool> TryBlah(string key, Action<int> value)
{
int something = await DoLongRunningIO();
value(something)
return true;
}
Использование:
int myOutParam = 0;
if (await TryBlah("Something", value => myOutParam = value))
{
// do somthing
}
Ответ 2
Вот около 2017 года обновление с ValueTuples, ваш вариант не так уж и плох.
public async Task<(bool, byte[])> TryBlahByteArray(string key)
{
// await something
return (true, new byte[1]);
}
public async Task<(bool, string)> TryBlahString(string key)
{
// await something
return (false, "blah");
}
Используется как
(bool success, byte[] blahs) = await TryBlahByteArray("key");
и
(bool success, string blah) = await TryBlahString("key");
Я не часто хочу, чтобы имена методов были одинаковыми и возвращали разные вещи или необработанный object
, так что, возможно, это не так важно. Ваш пробег может варьироваться.
Ответ 3
Я бы не использовал метод Try * с TPL. Вместо этого используйте продолжение (Task.ContinueWith) с опциями OnlyOnFaulted.
Таким образом ваша задача завершается так или иначе, и вызывающий получает решение о том, как обрабатывать ошибки, отмены и т.д.
Он также избавляется от Tuple.
Что касается других проблем дизайна, в любое время я вижу, что кто-то говорит: "Я хочу, чтобы этот метод перегружался на основе типа возврата". Я плохо отношусь к плохой идее. Я бы предпочел увидеть подробные имена (GetString, GetByte, GetByteArray и т.д. - посмотреть на SqlDataReader) или вернуть API очень простой тип (например, byte [] - посмотреть Stream) и позволить вызывающему абоненту создавать более высокие уровни, такие как StreamReader/ЧтениеТекста/др.
Ответ 4
Звучит как проблема для дженериков.
public async Task<Tuple<bool, TResult>> TryBlah<TResult>(string key)
{
var resultType = typeof(TResult);
// ... perform some slow io, return new Tuple<bool, TResult>(...)
}
Ответ 5
Похоже, вы пытаетесь сделать API, который принимает запрос, а затем извлекает некоторые данные, а затем обрабатывает или конвертирует эти данные определенным образом и возвращает его обратно вызывающему. Что делать, если вы реализовали менеджер, который будет обрабатывать различные методы обработки.
Я предлагаю решение создания класса запроса и ответа, которое будет передано диспетчеру, а затем менеджер вернет результат после завершения обработки.
public class Request
{
public Type ReturnType;
public string Key { get; set; }
public Request(string Key, Type returnType)
{
this.Key = Key;
this.ReturnType = returnType;
}
}
public class Response
{
public object value;
public Type returnType;
}
//Singleton processor to get data out of cache
public class CacheProcessor
{
private static CacheProcessor instance;
public static CacheProcessor Process
{
get
{
if (instance == null)
instance = new CacheProcessor();
return instance;
}
}
private Dictionary<Type, Func<Request, object>> Processors = new Dictionary<Type, Func<Request, object>>();
private CacheProcessor()
{
CreateAvailableProcessors();
}
//All available processors available here
//You could change type to string or some other type
//to extend if you need something like "CrazyZipUtility" as a processor
private void CreateAvailableProcessors()
{
Processors.Add(typeof(string), ProcessString);
Processors.Add(typeof(byte[]), ProcessByteArry);
}
//Fake method, this should encapsulate all crazy
//cache code to retrieve stuff out
private static string CacheGetKey(string p)
{
return "1dimd09823f02mf23f23f0"; //Bullshit data
}
//The goood old tryBlah... So Sexy
public Response TryBlah(Request request)
{
if (Processors.ContainsKey(request.ReturnType))
{
object processedObject = Processors[request.ReturnType].Invoke(request);
return new Response()
{
returnType = request.ReturnType,
value = processedObject
};
}
return null;
}
//Maybe put these in their own class along with the dictionary
//So you can maintain them in their own file
private static object ProcessString(Request request)
{
var value = CacheGetKey(request.Key);
//Do some shit
return value;
}
private static object ProcessByteArry(Request request)
{
var value = CacheGetKey(request.Key);
ASCIIEncoding encoding = new ASCIIEncoding();
Byte[] bytes = encoding.GetBytes(value);
return bytes;
}
}
Большая вещь - словарь (или HashSet) содержит ваши доступные процессоры. Затем, основываясь на типе, вызывается правильный процессор и возвращаются результаты.
Код будет вызываться следующим образом.
var makeByteRequest = new Request("SomeValue", typeof(byte[]));
Response btyeResponse = CacheProcessor.Process.TryBlah(makeByteRequest);