-
Notifications
You must be signed in to change notification settings - Fork 0
feat(utilities): add TryResult helpers #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,274 @@ | ||
| using System.Runtime.CompilerServices; | ||
|
|
||
| namespace ANcpLua.Roslyn.Utilities; | ||
|
|
||
| /// <summary> | ||
| /// Author-side helpers for <c>bool TryX(out T)</c> patterns: compact one-liners | ||
| /// that initialize <c>out</c> parameters on the failure or success return path. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Pairs with <see cref="TryExtensions" />, the consumer-side surface that turns | ||
| /// <c>bool + out</c> into <c>T?</c>. <see cref="TryResult" /> is the opposite direction: | ||
| /// it exists for code that <em>writes</em> a <c>bool TryFoo(out T)</c> method and wants to | ||
| /// collapse the failure or success branch from three or four lines to one. | ||
| /// </para> | ||
| /// <para> | ||
| /// All overloads request <see cref="MethodImplOptions.AggressiveInlining" />, keeping them close to a hand-written | ||
| /// <c>result = default; return false;</c> on the failure path or | ||
| /// <c>result = value; return true;</c> on the success path. | ||
| /// </para> | ||
| /// <para> | ||
| /// On .NET 9+, the type parameters carry <c>allows ref struct</c>, so the helpers also work when | ||
| /// an <c>out</c> parameter is a <see cref="System.Span{T}" />, <see cref="System.ReadOnlySpan{T}" />, | ||
| /// or any other ref struct — something the BCL's <c>Try*</c> shape can't express generically. | ||
| /// On <c>netstandard2.0</c> the relaxation is conditionally compiled out. | ||
| /// </para> | ||
| /// <para> | ||
| /// The <see cref="Fail{TReturn, T}(TReturn, out T)" /> family generalizes past <c>bool</c>-returning | ||
| /// try-methods: use it when the enclosing method returns an enum status, an int error code, or any | ||
| /// non-<c>bool</c> sentinel. | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <example> | ||
| /// <code> | ||
| /// // Multi-out failure path: from four lines to one. | ||
| /// public bool TryParse(string input, out Foo foo, out Bar bar) | ||
| /// { | ||
| /// if (!IsValid(input)) | ||
| /// return TryResult.Fail(out foo, out bar); | ||
| /// | ||
| /// foo = ParseFoo(input); | ||
| /// bar = ParseBar(input); | ||
| /// return true; | ||
| /// } | ||
| /// | ||
| /// // Single-out success and failure on one expression each. | ||
| /// public bool TryGetCached(string key, out Value result) | ||
| /// => _cache.TryGetValue(key, out var hit) | ||
| /// ? TryResult.Ok(hit, out result) | ||
| /// : TryResult.Fail(out result); | ||
| /// | ||
| /// // Non-bool return type. | ||
| /// public ParseStatus TryParse(string input, out Foo foo) | ||
| /// { | ||
| /// if (string.IsNullOrEmpty(input)) | ||
| /// return TryResult.Fail(ParseStatus.EmptyInput, out foo); | ||
| /// // ... | ||
| /// } | ||
| /// </code> | ||
| /// </example> | ||
| /// <seealso cref="TryExtensions" /> | ||
| #if ANCPLUA_ROSLYN_PUBLIC | ||
| public | ||
| #else | ||
| internal | ||
| #endif | ||
| static class TryResult | ||
| { | ||
| /// <summary> | ||
| /// Sets <paramref name="result" /> to <c>default</c> and returns <c>false</c>. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of the <c>out</c> parameter.</typeparam> | ||
| /// <param name="result">When this method returns, contains <c>default(T)</c>.</param> | ||
| /// <returns><c>false</c>.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static bool Fail<T>(out T? result) | ||
| #if NET9_0_OR_GREATER | ||
| where T : allows ref struct | ||
| #endif | ||
| { | ||
| result = default; | ||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets two <c>out</c> parameters to <c>default</c> and returns <c>false</c>. | ||
| /// </summary> | ||
| /// <typeparam name="T1">The type of the first <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T2">The type of the second <c>out</c> parameter.</typeparam> | ||
| /// <param name="result1">When this method returns, contains <c>default(T1)</c>.</param> | ||
| /// <param name="result2">When this method returns, contains <c>default(T2)</c>.</param> | ||
| /// <returns><c>false</c>.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static bool Fail<T1, T2>(out T1? result1, out T2? result2) | ||
| #if NET9_0_OR_GREATER | ||
| where T1 : allows ref struct | ||
| where T2 : allows ref struct | ||
| #endif | ||
| { | ||
| result1 = default; | ||
| result2 = default; | ||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets three <c>out</c> parameters to <c>default</c> and returns <c>false</c>. | ||
| /// </summary> | ||
| /// <typeparam name="T1">The type of the first <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T2">The type of the second <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T3">The type of the third <c>out</c> parameter.</typeparam> | ||
| /// <param name="result1">When this method returns, contains <c>default(T1)</c>.</param> | ||
| /// <param name="result2">When this method returns, contains <c>default(T2)</c>.</param> | ||
| /// <param name="result3">When this method returns, contains <c>default(T3)</c>.</param> | ||
| /// <returns><c>false</c>.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static bool Fail<T1, T2, T3>(out T1? result1, out T2? result2, out T3? result3) | ||
| #if NET9_0_OR_GREATER | ||
| where T1 : allows ref struct | ||
| where T2 : allows ref struct | ||
| where T3 : allows ref struct | ||
| #endif | ||
| { | ||
| result1 = default; | ||
| result2 = default; | ||
| result3 = default; | ||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets <paramref name="result" /> to <c>default</c> and returns <paramref name="returnValue" />. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Use when the enclosing <c>TryX</c> method returns something other than <c>bool</c> | ||
| /// (e.g. an enum status code, a parsed primary value, or a non-trivial sentinel). | ||
| /// </remarks> | ||
| /// <typeparam name="TReturn">The return type of the enclosing method.</typeparam> | ||
| /// <typeparam name="T">The type of the <c>out</c> parameter.</typeparam> | ||
| /// <param name="returnValue">The value to return.</param> | ||
| /// <param name="result">When this method returns, contains <c>default(T)</c>.</param> | ||
| /// <returns><paramref name="returnValue" />.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static TReturn Fail<TReturn, T>(TReturn returnValue, out T? result) | ||
| #if NET9_0_OR_GREATER | ||
| where TReturn : allows ref struct | ||
| where T : allows ref struct | ||
| #endif | ||
| { | ||
| result = default; | ||
| return returnValue; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets two <c>out</c> parameters to <c>default</c> and returns <paramref name="returnValue" />. | ||
| /// </summary> | ||
| /// <typeparam name="TReturn">The return type of the enclosing method.</typeparam> | ||
| /// <typeparam name="T1">The type of the first <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T2">The type of the second <c>out</c> parameter.</typeparam> | ||
| /// <param name="returnValue">The value to return.</param> | ||
| /// <param name="result1">When this method returns, contains <c>default(T1)</c>.</param> | ||
| /// <param name="result2">When this method returns, contains <c>default(T2)</c>.</param> | ||
| /// <returns><paramref name="returnValue" />.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static TReturn Fail<TReturn, T1, T2>(TReturn returnValue, out T1? result1, out T2? result2) | ||
| #if NET9_0_OR_GREATER | ||
| where TReturn : allows ref struct | ||
| where T1 : allows ref struct | ||
| where T2 : allows ref struct | ||
| #endif | ||
| { | ||
| result1 = default; | ||
| result2 = default; | ||
| return returnValue; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets three <c>out</c> parameters to <c>default</c> and returns <paramref name="returnValue" />. | ||
| /// </summary> | ||
| /// <typeparam name="TReturn">The return type of the enclosing method.</typeparam> | ||
| /// <typeparam name="T1">The type of the first <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T2">The type of the second <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T3">The type of the third <c>out</c> parameter.</typeparam> | ||
| /// <param name="returnValue">The value to return.</param> | ||
| /// <param name="result1">When this method returns, contains <c>default(T1)</c>.</param> | ||
| /// <param name="result2">When this method returns, contains <c>default(T2)</c>.</param> | ||
| /// <param name="result3">When this method returns, contains <c>default(T3)</c>.</param> | ||
| /// <returns><paramref name="returnValue" />.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static TReturn Fail<TReturn, T1, T2, T3>(TReturn returnValue, | ||
| out T1? result1, out T2? result2, out T3? result3) | ||
| #if NET9_0_OR_GREATER | ||
| where TReturn : allows ref struct | ||
| where T1 : allows ref struct | ||
| where T2 : allows ref struct | ||
| where T3 : allows ref struct | ||
| #endif | ||
| { | ||
| result1 = default; | ||
| result2 = default; | ||
| result3 = default; | ||
| return returnValue; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Assigns <paramref name="value" /> to <paramref name="result" /> and returns <c>true</c>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The success-path counterpart to <see cref="Fail{T}(out T)" />. Lets the success and failure | ||
| /// branches of a <c>TryX</c> method be expressed as conditional expressions rather than | ||
| /// <c>if</c>/<c>else</c> blocks. | ||
| /// </remarks> | ||
| /// <typeparam name="T">The type of the <c>out</c> parameter.</typeparam> | ||
| /// <param name="value">The value to assign.</param> | ||
| /// <param name="result">When this method returns, contains <paramref name="value" />.</param> | ||
| /// <returns><c>true</c>.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static bool Ok<T>(T value, out T result) | ||
| #if NET9_0_OR_GREATER | ||
| where T : allows ref struct | ||
| #endif | ||
| { | ||
| result = value; | ||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Assigns the supplied values to two <c>out</c> parameters and returns <c>true</c>. | ||
| /// </summary> | ||
| /// <typeparam name="T1">The type of the first <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T2">The type of the second <c>out</c> parameter.</typeparam> | ||
| /// <param name="value1">The value assigned to <paramref name="result1" />.</param> | ||
| /// <param name="value2">The value assigned to <paramref name="result2" />.</param> | ||
| /// <param name="result1">When this method returns, contains <paramref name="value1" />.</param> | ||
| /// <param name="result2">When this method returns, contains <paramref name="value2" />.</param> | ||
| /// <returns><c>true</c>.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static bool Ok<T1, T2>(T1 value1, T2 value2, out T1 result1, out T2 result2) | ||
| #if NET9_0_OR_GREATER | ||
| where T1 : allows ref struct | ||
| where T2 : allows ref struct | ||
| #endif | ||
| { | ||
| result1 = value1; | ||
| result2 = value2; | ||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Assigns the supplied values to three <c>out</c> parameters and returns <c>true</c>. | ||
| /// </summary> | ||
| /// <typeparam name="T1">The type of the first <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T2">The type of the second <c>out</c> parameter.</typeparam> | ||
| /// <typeparam name="T3">The type of the third <c>out</c> parameter.</typeparam> | ||
| /// <param name="value1">The value assigned to <paramref name="result1" />.</param> | ||
| /// <param name="value2">The value assigned to <paramref name="result2" />.</param> | ||
| /// <param name="value3">The value assigned to <paramref name="result3" />.</param> | ||
| /// <param name="result1">When this method returns, contains <paramref name="value1" />.</param> | ||
| /// <param name="result2">When this method returns, contains <paramref name="value2" />.</param> | ||
| /// <param name="result3">When this method returns, contains <paramref name="value3" />.</param> | ||
| /// <returns><c>true</c>.</returns> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static bool Ok<T1, T2, T3>(T1 value1, T2 value2, T3 value3, | ||
| out T1 result1, out T2 result2, out T3 result3) | ||
| #if NET9_0_OR_GREATER | ||
| where T1 : allows ref struct | ||
| where T2 : allows ref struct | ||
| where T3 : allows ref struct | ||
| #endif | ||
| { | ||
| result1 = value1; | ||
| result2 = value2; | ||
| result3 = value3; | ||
| return true; | ||
| } | ||
| } | ||
93 changes: 93 additions & 0 deletions
93
tests/ANcpLua.Roslyn.Utilities.Testing.Tests/TryResultTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| using ANcpLua.Roslyn.Utilities; | ||
| using AwesomeAssertions; | ||
| using Xunit; | ||
|
|
||
| namespace ANcpLua.Roslyn.Utilities.Testing.Tests; | ||
|
|
||
| public sealed class TryResultTests | ||
| { | ||
| [Fact] | ||
| public void Fail_DefaultsAllOutsAndReturnsFalse() | ||
| { | ||
| TryResult.Fail(out string? a).Should().BeFalse(); | ||
| a.Should().BeNull(); | ||
|
|
||
| TryResult.Fail(out int value).Should().BeFalse(); | ||
| value.Should().Be(default); | ||
|
|
||
| TryResult.Fail(out string? b1, out int? b2).Should().BeFalse(); | ||
| b1.Should().BeNull(); | ||
| b2.Should().BeNull(); | ||
|
|
||
| TryResult.Fail(out string? c1, out int? c2, out double? c3).Should().BeFalse(); | ||
| c1.Should().BeNull(); | ||
| c2.Should().BeNull(); | ||
| c3.Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void FailWithReturnValue_PropagatesReturnAndDefaultsOuts() | ||
| { | ||
| TryResult.Fail(ParseStatus.EmptyInput, out string? a).Should().Be(ParseStatus.EmptyInput); | ||
| a.Should().BeNull(); | ||
|
|
||
| TryResult.Fail(-1, out int? b1, out string? b2).Should().Be(-1); | ||
| b1.Should().BeNull(); | ||
| b2.Should().BeNull(); | ||
|
|
||
| TryResult.Fail("err", out int? c1, out int? c2, out int? c3).Should().Be("err"); | ||
| c1.Should().BeNull(); | ||
| c2.Should().BeNull(); | ||
| c3.Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Ok_AssignsValuesAndReturnsTrue() | ||
| { | ||
| TryResult.Ok(42, out int a).Should().BeTrue(); | ||
| a.Should().Be(42); | ||
|
|
||
| TryResult.Ok(1, "x", out int b1, out string b2).Should().BeTrue(); | ||
| b1.Should().Be(1); | ||
| b2.Should().Be("x"); | ||
|
|
||
| TryResult.Ok(1, "x", 3.14, out int c1, out string c2, out double c3).Should().BeTrue(); | ||
| c1.Should().Be(1); | ||
| c2.Should().Be("x"); | ||
| c3.Should().Be(3.14); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ComposesIntoReadableTryParse() | ||
| { | ||
| TryParseTwoInts("1,2", out var a, out var b).Should().BeTrue(); | ||
| a.Should().Be(1); | ||
| b.Should().Be(2); | ||
|
|
||
| TryParseTwoInts("oops", out a, out b).Should().BeFalse(); | ||
| a.Should().BeNull(); | ||
| b.Should().BeNull(); | ||
|
|
||
| TryParseTwoInts("1,bad", out a, out b).Should().BeFalse(); | ||
| a.Should().BeNull(); | ||
| b.Should().BeNull(); | ||
| } | ||
|
|
||
| private static bool TryParseTwoInts(string input, out int? first, out int? second) | ||
| { | ||
| var parts = input.Split(','); | ||
| if (parts.Length != 2 | ||
| || !int.TryParse(parts[0], out var parsedFirst) | ||
| || !int.TryParse(parts[1], out var parsedSecond)) | ||
| return TryResult.Fail(out first, out second); | ||
|
|
||
| return TryResult.Ok<int?, int?>(parsedFirst, parsedSecond, out first, out second); | ||
| } | ||
|
|
||
| private enum ParseStatus | ||
| { | ||
| Ok, | ||
| EmptyInput, | ||
| Invalid | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
FailAPI currently takesout T?, which makes common try-pattern signatures noisy or warning-prone in nullable-enabled projects: callingTryResult.Fail(out result)from methods that exposeout string result(or genericout T result) produces nullability-mismatch diagnostics and forces callers to either relax their public signature to nullable or suppress warnings. This undercuts the intended one-liner helper for standard[MaybeNullWhen(false)] out Ttry methods, especially when warnings are treated as errors.Useful? React with 👍 / 👎.