How to implement the TryDoSomething pattern with a

2020-06-22 05:39发布

问题:

I often use the TryDoSomething pattern like this totally made up example:

GameContext gameContext;
if (gamesRepository.TryLoadLastGame(out gameContext))
{
    // Perform actions with gameContext instance.
}
else
{
    // Create a new game or go to home screen or whatever.
}

This allows a nice readable flow but it also allows a success status true but a null return value which is useful sometimes for communicating "I was able to get you your thing but its actually null".

With async-await, the asynchronous lower APIs "force" calling APIs to do the right thing and work asynchronously. However, without out parameters, this pattern doesn't work.

How can it be achieved?

I have an answer, and am answering Q/A style to see how other people think about it.

回答1:

So far I've used a little class called Attempt<T>:

public sealed class Attempt<T>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Attempt{T}"/> class.
    /// </summary>
    public Attempt() { }

    /// <summary>
    /// Initializes a new instance of the <see cref="Attempt{T}"/> class.
    /// </summary>
    public Attempt(Exception exception)
    {
        this.Exception = exception;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="Attempt{T}"/> class.
    /// </summary>
    /// <param name="result">The result.</param>
    public Attempt(T result)
    {
        this.Result = result;
        this.HasResult = true;
    }

    /// <summary>
    /// Gets the result.
    /// </summary>
    /// <value>The result.</value>
    public T Result { get; private set; }

    /// <summary>
    /// Determines whether this instance has result.
    /// </summary>
    /// <returns><c>true</c> if this instance has result; otherwise, <c>false</c>.</returns>
    public bool HasResult { get; private set; }

    /// <summary>
    /// Returns the result with a true or false depending on whether its empty, but throws if there is an exception in the attempt.
    /// </summary>
    /// <param name="result">The result, which may be null.</param>
    /// <returns><c>true</c> if the specified result has a value; otherwise, <c>false</c>.</returns>
    /// <exception cref="System.AggregateException">The attempt resulted in an exception. See InnerExceptions.</exception>
    public bool TryResult(out T result)
    {
        if (this.HasResult)
        {
            result = this.Result;
            return true;
        }
        else
        {
            if (this.Exception != null)
            {
                throw new AggregateException("The attempt resulted in an exception. See InnerExceptions.", this.Exception);
            }
            else
            {
                result = default(T);
                return false;
            }
        }
    }

    /// <summary>
    /// Gets or sets the exception.
    /// </summary>
    /// <value>The exception.</value>
    public Exception Exception { get; private set; }
}

Which is used within a Try method like this:

internal async Task<Attempt<T>> TryReadObjectAsync<T>(string folderName, string fileName)
{
    if (String.IsNullOrWhiteSpace(folderName))
        throw new ArgumentException("The string argument is null or whitespace.", "folderName");

    if (String.IsNullOrWhiteSpace(fileName))
        throw new ArgumentException("The string argument is null or whitespace.", "fileName");

    try
    {
        StorageFolder folder = this.StorageFolder;                
        if (folderName != @"\")
            folder = await StorageFolder.GetFolderAsync(folderName);

        var file = await folder.GetFileAsync(fileName);
        var buffy = await FileIO.ReadBufferAsync(file);

        string xml = await buffy.ReadUTF8Async();

        T obj = await Evoq.Serialization.DataContractSerializerHelper.DeserializeAsync<T>(xml);

        return new Attempt<T>(obj);
    }
    catch (FileNotFoundException fnfe)
    {
        // Only catch and wrap expected exceptions.

        return new Attempt<T>(fnfe);
    }
}

And is consumed like so:

DataContractStorageSerializer xmlStorage = new DataContractStorageSerializer(this.StorageFolder);

var readAttempt = await xmlStorage.TryReadObjectAsync<UserProfile>(userName, UserProfileFilename);

try
{
    UserProfile user;
    if (readAttempt.TryResult(out user))
    {
        // Do something with user.
    }
    else
    {
        // No user in the persisted file.
    }
}
catch (Exception ex)
{
    // Some unexpected, unhandled exception happened.
}

Or, ignoring errors.

...
var readAttempt = await xmlStorage.TryReadObjectAsync<UserProfile>(userName, UserProfileFilename);

if (readAttempt.HasResult)
{
    // Continue.

    this.DoSomethingWith(readAttempt.Result);
}
else
{
    // Create new user.
}


回答2:

For comparison purposes, here's a similar approach that I employ:

/// <summary>
/// Encapsulates the return value from a method that has a value, error status and optional error message.
/// This is to be used in cases where a method call can fail, but you don't want to throw an exception;
/// for example, because you have an error message string that you want to be displayed to the user.
/// </summary>
/// <typeparam name="T">The type of the main return value.</typeparam>

[DataContract]

public class Result<T>
{
    /// <summary>Creates a failure result without an error message or an exception.</summary>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure()
    {
        return new Result<T>(default(T), false, string.Empty, null, false);
    }

    /// <summary>Creates a failure result with an error message but no exception.</summary>
    /// <param name="errorMessage">
    /// The optional error message. This may not be null. 
    /// It may however be <see cref="String.Empty"/>.
    /// </param>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure(string errorMessage)
    {
        return new Result<T>(default(T), false, errorMessage, null, false);
    }

    /// <summary>Creates a failure result with an exception but no error message.</summary>
    /// <param name="exception">The exception. This may be null.</param>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure(Exception exception)
    {
        return new Result<T>(default(T), false, string.Empty, exception, false);
    }

    /// <summary>Creates a failure result with an error message and an exception.</summary>
    /// <param name="errorMessage">
    /// The optional error message. This may not be null.
    /// It may however be <see cref="String.Empty"/>.
    /// </param>
    /// <param name="exception">The exception. This may be null.</param>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure(string errorMessage, Exception exception)
    {
        return new Result<T>(default(T), false, errorMessage, exception, false);
    }

    /// <summary>Creates a failure result without an error message or an exception.</summary>
    /// <param name="value">The result value.</param>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure(T value)
    {
        return new Result<T>(value, false, string.Empty, null, true);
    }

    /// <summary>Creates a failure result with an error message but no exception.</summary>
    /// <param name="errorMessage">
    /// The optional error message. This may not be null. 
    /// It may however be <see cref="String.Empty"/>.
    /// </param>
    /// <param name="value">The result value.</param>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure(string errorMessage, T value)
    {
        return new Result<T>(value, false, errorMessage, null, true);
    }

    /// <summary>Creates a failure result with an exception but no error message.</summary>
    /// <param name="exception">The exception. This may be null.</param>
    /// <param name="value">The result value.</param>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure(Exception exception, T value)
    {
        return new Result<T>(value, false, string.Empty, exception, true);
    }

    /// <summary>Creates a failure result with an error message and an exception.</summary>
    /// <param name="errorMessage">
    /// The optional error message. This may not be null.
    /// It may however be <see cref="String.Empty"/>.
    /// </param>
    /// <param name="exception">The exception. This may be null.</param>
    /// <param name="value">The result value.</param>
    /// <returns>A failure result value.</returns>

    public static Result<T> Failure(string errorMessage, Exception exception, T value)
    {
        return new Result<T>(value, false, errorMessage, exception, true);
    }

    /// <summary>Creates a success result.</summary>
    /// <param name="value">The successful value.</param>
    /// <returns>A success result value.</returns>

    public static Result<T> Success(T value)
    {
        return new Result<T>(value, true, string.Empty, null, true);
    }

    /// <summary>
    /// The error message, if applicable and if <see cref="IsSuccessful"/> is false.
    /// This cannot be null, but it may be <see cref="String.Empty"/>.
    /// This is meaningless if <see cref="IsSuccessful"/> is true.
    /// Do not call this if <see cref="IsSuccessful"/> is true.
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown if this is called when <see cref="IsSuccessful"/> is true.
    /// </exception>

    public string ErrorMessage
    {
        get
        {
            if (IsSuccessful)
            {
                throw new InvalidOperationException("You cannot access the error message if the method call was successful.");
            }

            return _errorMessage;
        }
    }

    /// <summary>
    /// The primary return value from the method. 
    /// This is meaningless if <see cref="HasValue"/> is false.
    /// Do not call this if <see cref="HasValue"/> is false.
    /// (If <see cref="IsSuccessful"/> is true, then <see cref="HasValue"/> will also be true.)
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown if this is called when <see cref="HasValue"/> is false.
    /// </exception>

    public T Value
    {
        get
        {
            if (!HasValue)
            {
                throw new InvalidOperationException("You cannot access the result value if the method call failed and did not set a result value.");
            }

            return _value;
        }
    }

    /// <summary>
    /// Does the result have a valid value?
    /// If <see cref="IsSuccessful"/> is true then this will also be true.
    /// A result cannot be successful without having an associated valid result value.
    /// </summary>

    public bool HasValue
    {
        get
        {
            return _hasValue;
        }
    }

    /// <summary>
    /// Was the method call successful?
    /// If this is true, then <see cref="HasValue"/> will also be true. 
    /// A result cannot be successful without having an associated valid result value.
    /// </summary>

    public bool IsSuccessful
    {
        get
        {
            return _isSuccessful;
        }
    }

    /// <summary>
    /// The optional <see cref="Exception"/> associated with a failure, if applicable.
    /// This may be null.
    /// Do not call this if <see cref="IsSuccessful"/> is true.
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown if this is called when <see cref="IsSuccessful"/> is true.
    /// </exception>

    public Exception Exception
    {
        get
        {
            if (IsSuccessful)
            {
                throw new InvalidOperationException("You cannot access the exception if the method call was successful.");
            }

            return _exception;
        }
    }

    /// <summary>Constructor.</summary>
    /// <param name="value">T</param>
    /// <param name="isSuccessful">Was the method call successful?</param>
    /// <param name="errorMessage">The optional error message. This may not be null.
    /// It must be <see cref="string.Empty"/> if <paramref name="isSuccessful"/> is true.</param>
    /// <param name="exception">The exception, if applicable. Must be null if <paramref name="isSuccessful"/> is true.</param>
    /// <param name="isValueValid">Is <paramref name="value"/> valid?</param>

    private Result(T value, bool isSuccessful, string errorMessage, Exception exception, bool isValueValid)
    {
        if (errorMessage == null)
        {
            throw new ArgumentNullException("errorMessage");
        }

        if (isSuccessful && !string.IsNullOrEmpty(errorMessage))
        {
            throw new ArgumentOutOfRangeException("errorMessage", errorMessage, "errorMessage must be empty if isSuccessful is true.");
        }

        if (isSuccessful && (exception != null))
        {
            throw new ArgumentOutOfRangeException("exception", exception, "exception must be null if isSuccessful is true.");
        }

        _value        = value;
        _exception    = exception;
        _isSuccessful = isSuccessful;
        _hasValue     = isValueValid;
        _errorMessage = errorMessage;
    }


    [DataMember] private readonly T         _value;
    [DataMember] private readonly string    _errorMessage;
    [DataMember] private readonly bool      _isSuccessful;
    [DataMember] private readonly bool      _hasValue;
    [DataMember] private readonly Exception _exception;
}

/// <summary>
/// Encapsulates the return value from a method that has an error status and optional error message, 
/// but no actual return value.
/// This is to be used in cases where a method call can fail, but you don't want to throw an exception;
/// for example, because you have an error message string that you want to be displayed to the user.
/// </summary>

[DataContract]

public class Result
{
    /// <summary>Creates a failure result without an error message or an exception.</summary>
    /// <returns>A failure result value.</returns>

    public static Result Failure()
    {
        return new Result(false, string.Empty, null);
    }

    /// <summary>Creates a failure result with an error message but no exception.</summary>
    /// <param name="errorMessage">
    /// The optional error message. This may not be null. 
    /// It may however be <see cref="String.Empty"/>.
    /// </param>
    /// <returns>A failure result value.</returns>

    public static Result Failure(string errorMessage)
    {
        return new Result(false, errorMessage, null);
    }

    /// <summary>Creates a failure result with an exception but no error message.</summary>
    /// <param name="exception">The exception. This may be null.</param>
    /// <returns>A failure result value.</returns>

    public static Result Failure(Exception exception)
    {
        return new Result(false, string.Empty, exception);
    }

    /// <summary>Creates a failure result with an error message and an exception.</summary>
    /// <param name="errorMessage">
    /// The optional error message. This may not be null.
    /// It may however be <see cref="String.Empty"/>.
    /// </param>
    /// <param name="exception">The exception. This may be null.</param>
    /// <returns>A failure result value.</returns>

    public static Result Failure(string errorMessage, Exception exception)
    {
        return new Result(false, errorMessage, exception);
    }

    /// <summary>Creates a success result.</summary>
    /// <returns>A success result value.</returns>

    public static Result Success()
    {
        return new Result(true, string.Empty, null);
    }

    /// <summary>
    /// The error message, if applicable and if <see cref="IsSuccessful"/> is false.
    /// This cannot be null, but it may be <see cref="String.Empty"/>.
    /// This is meaningless if <see cref="IsSuccessful"/> is true.
    /// Do not call this if <see cref="IsSuccessful"/> is true.
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown if this is called when <see cref="IsSuccessful"/> is true.
    /// </exception>

    public string ErrorMessage
    {
        get
        {
            if (IsSuccessful)
            {
                throw new InvalidOperationException("You cannot access the error message if the method call was successful.");
            }

            return _errorMessage;
        }
    }

    /// <summary>Was the method call successful?</summary>

    public bool IsSuccessful
    {
        get
        {
            return _isSuccessful;
        }
    }

    /// <summary>
    /// The optional <see cref="Exception"/> associated with a failure, if applicable.
    /// This may be null.
    /// Do not call this if <see cref="IsSuccessful"/> is true.
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown if this is called when <see cref="IsSuccessful"/> is true.
    /// </exception>

    public Exception Exception
    {
        get
        {
            if (IsSuccessful)
            {
                throw new InvalidOperationException("You cannot access the exception if the method call was successful.");
            }

            return _exception;
        }
    }

    /// <summary>Constructor.</summary>
    /// <param name="isSuccessful">Was the method call successful?</param>
    /// <param name="errorMessage">The optional error message. This may not be null.
    /// It must be <see cref="string.Empty"/> if <paramref name="isSuccessful"/> is true.</param>
    /// <param name="exception">The exception, if applicable. Must be null if <paramref name="isSuccessful"/> is true.</param>

    private Result(bool isSuccessful, string errorMessage, Exception exception)
    {
        if (errorMessage == null)
        {
            throw new ArgumentNullException("errorMessage");
        }

        if (isSuccessful && !string.IsNullOrEmpty(errorMessage))
        {
            throw new ArgumentOutOfRangeException("errorMessage", errorMessage, "errorMessage must be empty if isSuccessful is true.");
        }

        if (isSuccessful && (exception != null))
        {
            throw new ArgumentOutOfRangeException("exception", exception, "exception must be null if isSuccessful is true.");
        }

        _exception    = exception;
        _isSuccessful = isSuccessful;
        _errorMessage = errorMessage;
    }


    [DataMember] private readonly string    _errorMessage;
    [DataMember] private readonly bool      _isSuccessful;
    [DataMember] private readonly Exception _exception;
}