jIAnSoft.Rayleigh
26.7.26
dotnet add package jIAnSoft.Rayleigh --version 26.7.26
NuGet\Install-Package jIAnSoft.Rayleigh -Version 26.7.26
<PackageReference Include="jIAnSoft.Rayleigh" Version="26.7.26" />
<PackageVersion Include="jIAnSoft.Rayleigh" Version="26.7.26" />
<PackageReference Include="jIAnSoft.Rayleigh" />
paket add jIAnSoft.Rayleigh --version 26.7.26
#r "nuget: jIAnSoft.Rayleigh, 26.7.26"
#:package jIAnSoft.Rayleigh@26.7.26
#addin nuget:?package=jIAnSoft.Rayleigh&version=26.7.26
#tool nuget:?package=jIAnSoft.Rayleigh&version=26.7.26
English | 繁體中文
Rayleigh
Option and Result types for C#, inspired by Rust.
A zero-allocation, high-performance functional primitives library for .NET 8 and .NET 10.
Why Rayleigh?
The C# nullable reference types feature is useful,
but it's entirely optional and easily ignored. Result<T, E> goes further by making error handling explicit in the type system.
Rayleigh uses the type system to:
- Prevent null-reference errors — Make it impossible to access a possibly-missing value without first checking if the value is present.
- Express intent clearly — If a method might not return a value, or might return an error, the return type makes this obvious and impossible to miss.
- Chain operations safely — Use
Map,Bind, and other combinators to build pipelines that short-circuit onNoneorErr, eliminating nestedif/elseblocks. - Avoid exception-driven control flow — Use
Result<T, E>for expected business errors (validation, not-found, etc.), reserving exceptions for truly unexpected failures.
Installation
dotnet add package jIAnSoft.Rayleigh
Or via the NuGet Package Manager:
Install-Package jIAnSoft.Rayleigh
When to Use Option vs Result
Use Option<T> when absence is a normal outcome and the caller does not need to know why the value is missing.
Use Result<T, E> when failure needs an explanation that the caller may log, display, return from an API, retry, or branch on.
| Situation | Use |
|---|---|
| A lookup may or may not find a value | Option<T> |
| A nullable value should become explicit | Option<T> |
| Missing value is expected and not an error | Option<T> |
| Validation, authorization, parsing, or I/O can fail | Result<T, E> |
| The caller needs an error reason | Result<T, E> |
| The failure should be logged, displayed, returned, retried, or handled differently by type | Result<T, E> |
In short: Option<T> means maybe value; Result<T, E> means success or explained failure.
Usage
Creating an Option
using jIAnSoft.Rayleigh;
// From a value
var some = Option<int>.Some(42);
var none = Option<int>.None;
// Using implicit conversions (recommended)
Option<int> implicitSome = 42; // implicitly Some(42)
Option<int> implicitNone = Option.None; // universal None marker
// From nullable reference types
string? name = GetName();
var option = name.ToOption(); // Some("Alice") or None
// From nullable value types
int? maybeAge = GetAge();
var ageOption = maybeAge.ToOption(); // Some(25) or None
Getting Values from an Option
// TryGetValue — guard-clause style
if (!option.TryGetValue(out var value))
{
return; // early exit, no value
}
// value is guaranteed non-null here
// Match — exhaustive handling of both cases
var message = option.Match(
some: v => $"Hello, {v}!",
none: () => "Hello, Guest!"
);
// UnwrapOr — provide a default
var name = option.UnwrapOr("Unknown");
// Deconstruct — pattern matching in switch
var result = option switch
{
(true, var v) => $"Value: {v}",
(false, _) => "No value"
};
Creating a Result
using jIAnSoft.Rayleigh;
// Using factory methods
var ok = Result<int, string>.Ok(42);
var err = Result<int, string>.Err("Something went wrong");
// Using implicit conversions (recommended)
Result<int, string> Divide(int a, int b)
{
if (b == 0) return "Division by zero"; // implicitly Err
return a / b; // implicitly Ok
}
// Using Ok<T> / Err<E> wrapper records
Result<int, string> fromWrapper = new Ok<int>(42);
Result<int, string> fromErrWrapper = new Err<string>("Oops");
Getting Values from a Result
// TryGetOk — guard-clause style (recommended)
if (!result.TryGetOk(out var value, out var error))
{
return BadRequest(error); // early exit with error
}
// value is guaranteed non-null here
// Match — exhaustive handling
var response = result.Match(
ok: user => $"Welcome, {user.Name}!",
err: error => $"Error: {error}"
);
// UnwrapOr — provide a default
var timeout = GetConfig("Timeout").UnwrapOr(30);
Safely Chain Together Fallible Methods
Rayleigh supports safely chaining together multiple methods that return Option or Result,
and converting between Option and Result when needed.
var output = ValidateInput(userInput)
.Bind(validated => FindUser(validated.UserId))
.Filter(user => user.IsActive)
.Map(user => user.Name)
.UnwrapOr("Guest");
The example above does the following:
- Validates the user input. If validation fails, returns
None/Err. - If validation succeeds, looks up the user. If the user is not found, returns
None. - If found, checks whether the user is active. If not, becomes
None. - If the user is active, extracts the name.
- If at the end we have a value, returns it. Otherwise, returns
"Guest".
At no point is a null reference possible, and there are no nested if / else blocks.
Railway-Oriented Programming with Result
// Define methods that return Result
Result<UserDto, AppError> CreateUser(CreateUserRequest request)
{
return ValidateName(request.Name)
.Bind(name => ValidateEmail(request.Email).Map(email => (name, email)))
.Bind(pair => SaveToDatabase(pair.name, pair.email))
.Map(entity => entity.ToDto())
.Tap(dto => logger.LogInformation("Created user {Id}", dto.Id))
.TapErr(err => logger.LogError("Failed: {Error}", err));
}
// LINQ query syntax — equivalent to chained Bind calls
var total = from user in GetUser(userId)
from order in GetLatestOrder(user.Id)
select order.Total;
Async Pipelines
When your operations involve I/O (database queries, HTTP calls, file access, etc.),
you need async versions of the combinators. Rayleigh provides Task and ValueTask
extension methods that let you chain async operations with the same railway-oriented style —
errors propagate automatically, and you never need nested try/catch or if blocks.
BindAsync — Chain async operations that may fail
BindAsync takes a function that returns Task<Result<TU, TE>> (or Task<Option<TU>>).
If the source is Ok/Some, the function is called; if it's Err/None, the chain short-circuits.
// Each step returns Task<Result<T, E>>, errors propagate automatically
var user = await ValidateTokenAsync(token) // Task<Result<UserId, ApiError>>
.BindAsync(id => FindUserAsync(id)) // -> Task<Result<User, ApiError>>
.BindAsync(user => LoadPermissionsAsync(user)) // -> Task<Result<UserWithPerms, ApiError>>
MapAsync — Transform the success value asynchronously
MapAsync transforms the inner value without changing the Result/Option structure.
Unlike BindAsync, the mapper returns a plain Task<TU>, not a wrapped type.
var dto = await GetUserAsync(userId) // Task<Result<User, string>>
.MapAsync(user => EnrichWithAvatarAsync(user)) // -> Task<Result<UserDto, string>>
TapAsync / TapErrAsync — Async side effects
Execute async side effects (logging, notifications, metrics) without changing the value.
TapAsync fires on success, TapErrAsync fires on error.
var result = await CreateOrderAsync(request)
.TapAsync(order => SendConfirmationEmailAsync(order))
.TapErrAsync(err => AlertOpsChannelAsync(err));
// result is unchanged — side effects run but don't alter the value
OrElseAsync — Async fallback on error
Provide an alternative when the original operation fails.
var data = await LoadFromCacheAsync(key)
.OrElseAsync(err => LoadFromDatabaseAsync(key));
// If cache misses (Err), falls back to the database
MapErrAsync — Transform the error asynchronously
Convert one error type to another (e.g., enrich with context from an async source).
var result = await CallExternalApiAsync(request)
.MapErrAsync(err => EnrichErrorWithTraceAsync(err));
Full example — Combining everything
Result<OrderConfirmation, AppError> confirmation = await ValidateOrderAsync(request)
.BindAsync(order => CheckInventoryAsync(order))
.BindAsync(order => ProcessPaymentAsync(order))
.MapAsync(receipt => BuildConfirmationAsync(receipt))
.TapAsync(conf => SendEmailAsync(conf.Email, conf))
.TapErrAsync(err => logger.LogErrorAsync("Order failed: {Error}", err))
.OrElseAsync(err => CreatePendingOrderAsync(request, err));
The pipeline above:
- ValidateOrderAsync — Validates the incoming request. Returns
Errif invalid. - CheckInventoryAsync — Checks stock availability. Returns
Errif out of stock. - ProcessPaymentAsync — Charges the customer. Returns
Errif payment fails. - BuildConfirmationAsync — Transforms the payment receipt into a confirmation DTO.
- SendEmailAsync — Sends a confirmation email (side effect, doesn't alter the value).
- LogErrorAsync — Logs the error if any step failed (side effect on the error path).
- CreatePendingOrderAsync — Fallback: if anything failed, create a pending order instead.
Each step only runs if the previous one succeeded. Errors propagate automatically without any try/catch or if/else nesting.
TaskvsValueTask: Every method above has aValueTaskoverload as well. UseValueTaskin hot paths to avoid heap allocation when the result is often available synchronously.
CancellationToken: Every async combinator also has an overload whose delegate accepts aCancellationTokenas its last parameter, plus a trailingCancellationTokenargument on the combinator itself:var result = await GetUserAsync(userId) .BindAsync((user, ct) => GetOrdersAsync(user.Id, ct), cancellationToken);The token is only checked (and forwarded) when the delegate is actually about to run — a short-circuited
Err/Nonebranch never observes cancellation, matching the no-op semantics of the rest of the library.
Working with Collections
Entering the Option world — safely
The BCL's FirstOrDefault and GetValueOrDefault cannot distinguish "not found" from
"found a value that happens to equal the default". For value types this silently loses information:
var scores = new[] { 0, 5, 10 };
scores.FirstOrDefault(); // 0 ─┬─ indistinguishable
Array.Empty<int>().FirstOrDefault(); // 0 ─┘
scores.FirstOrNone(); // Some(0)
Array.Empty<int>().FirstOrNone(); // None
The same applies to dictionaries:
var counts = new Dictionary<string, int> { ["a"] = 0 };
counts.GetValueOrDefault("a"); // 0 ─┬─ indistinguishable
counts.GetValueOrDefault("miss"); // 0 ─┘
counts.GetValueOrNone("a"); // Some(0)
counts.GetValueOrNone("miss"); // None
Full set of entry points:
source.FirstOrNone(); // Option<T>
source.FirstOrNone(x => x > 10); // Option<T>, short-circuits on first match
source.SingleOrNone(); // None if empty OR more than one (never throws)
source.ElementAtOrNone(3); // None if out of range (negative index included)
dictionary.GetValueOrNone(key); // Option<TValue>
Sequence() — all-or-nothing
Turns a collection of Option/Result inside out. Short-circuits on the first None/Err:
Option<int>[] all = [Option<int>.Some(1), Option<int>.Some(2)];
Option<int>[] some = [Option<int>.Some(1), Option<int>.None];
all.Sequence(); // Some([1, 2])
some.Sequence(); // None
// With Result: returns the FIRST error encountered, and stops enumerating there
var validated = inputs.Select(Validate).Sequence(); // Result<List<Valid>, Error>
Partition() — collect every error
Where Sequence() short-circuits, Partition() walks the entire sequence and gathers everything.
This is what you want for form validation, where the user should see all problems at once:
var (users, errors) = dtos.Select(Validate).Partition();
if (errors.Count > 0)
{
return BadRequest(errors); // report ALL validation failures, not just the first
}
return Ok(users);
Values() — keep the Somes, drop the Nones
Option<int>[] options = [Option<int>.Some(1), Option<int>.None, Option<int>.Some(3)];
options.Values(); // [1, 3]
Values()vsSequence()Values()ignoresNoneand keeps what it can.Sequence()treats anyNoneas total failure.
Starting an Async Pipeline from a Sync Value
The Task/ValueTask async overloads cover the case where the pipeline already is async. When the
start of the chain is a synchronously obtained Result/Option, use the overloads that take the
value itself:
// Before — an extra Task allocation just to satisfy the type
await Task.FromResult(Validate(input)).BindAsync(v => SaveAsync(v));
// After
await Validate(input).BindAsync(v => SaveAsync(v));
These overloads are deliberately not declared async. On the short-circuit path (Err/None) they
return an already-completed ValueTask — no state machine, no allocation:
var pending = Result<int, string>.Err("boom")
.BindAsync(x => new ValueTask<Result<int, string>>(Result<int, string>.Ok(x)));
pending.IsCompletedSuccessfully; // true — never touched the thread pool
Unit Type
Use Unit as a success type in Result when there is no meaningful return value:
Result<Unit, string> Save(Entity entity)
{
if (!IsValid(entity)) return "Validation failed";
repository.Save(entity);
return Unit.Value;
}
Converting Between Option and Result
// Option -> Result (None becomes Err with provided error)
var result = option.ToResult("Value not found");
var result2 = option.ToResult(() => new AppError("Not found"));
// Result -> Option (discards error info)
var option = result.ToOption();
// Result -> Option<Error> (discards success value)
var maybeError = result.Err();
Uses Modern .NET Features
- Zero heap allocation — All core types (
Option<T>,Result<T, E>,Unit) arereadonly struct, living entirely on the stack. - AggressiveInlining — All critical-path methods are JIT-inlined for minimal overhead.
IEquatable<T>andIComparable<T>—OptionandUnitcan be compared, sorted, and used as dictionary keys.- Nullable annotations — Full support for C# nullable reference type analysis.
- LINQ query syntax —
Select,SelectMany,Whereenablefrom/where/selectsyntax. - Pattern matching —
Deconstructenablesswitchexpressions andispatterns. - Async support —
Task<T>andValueTask<T>extension methods forBindAsync,MapAsync,OrElseAsync,TapAsync, and more. - Implicit conversions —
Result<T, E>can be created directly fromT,E,Ok<T>, orErr<E>for concise method returns. - Poisoned default state —
default(Result<T, E>)is not a valid result. Because C# lets any struct be zero-initialized (array slots, unassigned fields),Resulttracks a distinct uninitialized state and throwsInvalidOperationExceptionon any member that would read a value. This holds for everyE, includingenumandstructerror types where the default value is an ordinary member. - AOT & trimming ready —
IsAotCompatibleandIsTrimmable; no reflection, no dynamic code generation.
Known limitation —
TandEmust differResult<T, E>declares implicit conversions from bothTandE. When they resolve to the same type (e.g.Result<string, string>), those two operators collide and implicit conversion fails to compile withCS0457. Use the explicit factories or the wrapper records instead:Result<string, string> r = ok ? new Ok<string>("value") : new Err<string>("error");Better still, give errors a dedicated type (
enum,record, orreadonly record struct) — it sidesteps the limitation and gives failures real meaning in the type system.
API Reference
Option<T>
| Method | Description |
|---|---|
Some(T) / None |
Construction |
IsSome / IsNone |
State check |
Contains(T) |
Value equality check |
IsSomeAnd(predicate) |
Conditional check |
Match(some, none) |
Pattern match (with or without return value) |
Map(mapper) |
Transform inner value |
Filter(predicate) |
Conditional passthrough |
Bind(binder) |
Monadic bind (flatMap) |
Zip(other) / ZipWith(other, zipper) |
Combine two Options |
Or(other) / OrElse(factory) |
Fallback |
Tap(action) |
Side effect without changing value |
Unwrap() / UnwrapOr(default) / UnwrapOrElse(factory) / Expect(msg) |
Extract value |
TryGetValue(out value) |
TryParse-style extraction |
ToResult(error) / ToResult(factory) |
Convert to Result |
MapOr(default, mapper) / MapOrElse(factory, mapper) |
Map with fallback |
Deconstruct(out isSome, out value) |
Deconstruct for pattern matching and switch expressions |
Select / SelectMany / Where |
LINQ support |
Equals / CompareTo / comparison operators |
Equality, ordering, and sorting support; None sorts before Some |
ToString() |
Debug-friendly Some(value) or None text |
Flatten() |
Unwrap nested Option<Option<T>> (extension) |
Result<T, E>
| Method | Description |
|---|---|
Ok(T) / Err(E) |
Construction |
Implicit from T / E / Ok<T> / Err<E> |
Implicit conversions |
IsOk / IsErr |
State check |
Contains(T) / ContainsErr(E) |
Value / error equality check |
IsOkAnd(predicate) / IsErrAnd(predicate) |
Conditional check |
Match(ok, err) |
Pattern match |
Map(mapper) / MapErr(mapper) |
Transform value or error |
Bind(binder) |
Monadic bind |
Or(other) / OrElse(factory) |
Fallback |
Tap(action) / TapErr(action) |
Side effects |
Unwrap() / UnwrapOr(default) / UnwrapOrElse(factory) / Expect(msg) |
Extract value |
UnwrapErr() / ExpectErr(msg) |
Extract error |
TryGetOk(out value) / TryGetOk(out value, out error) / TryGetErr(out error) |
TryParse-style |
ToOption() / Err() |
Convert to Option |
MapOr / MapOrElse |
Map with fallback |
Deconstruct(out isOk, out value, out error) |
Deconstruct for pattern matching and switch expressions |
Select / SelectMany |
LINQ support |
Equals / CompareTo / comparison operators |
Equality, ordering, and sorting support; Err sorts before Ok |
ToString() |
Debug-friendly Ok(value) or Err(error) text |
Flatten() |
Unwrap nested Result<Result<T,E>,E> (extension) |
Supporting Types
| Type | API | Description |
|---|---|---|
Unit |
Unit.Value |
Represents a successful result with no meaningful value, useful as Result<Unit, E> |
Unit |
Equals / CompareTo / == / != / ToString() |
All Unit values are equal, compare as equal, and render as () |
OptionNone |
Option.None marker |
Universal None marker that can implicitly convert to any Option<T> |
Ok<T> / Err<E> |
Wrapper records | Explicit wrappers for creating Result<T, E> when implicit conversion is ambiguous or clarity matters |
Extension Methods
| Class | Method | Description |
|---|---|---|
OptionExtensions |
OrNull() |
Convert Option<T> to T? (supports both struct and class types) |
NullableExtensions |
ToOption() |
Convert T? (reference or value type) to Option<T> |
EnumerableExtensions |
Values() |
Filter IEnumerable<Option<T>> to extract all Some values |
EnumerableExtensions |
FirstOrNone() / FirstOrNone(predicate) |
First element as Option<T>; None if empty or no match |
EnumerableExtensions |
SingleOrNone() |
The single element as Option<T>; None if empty or more than one |
EnumerableExtensions |
ElementAtOrNone(index) |
Element at index as Option<T>; None if out of range |
EnumerableExtensions |
GetValueOrNone(key) |
Dictionary lookup as Option<TValue>; None if the key is absent |
EnumerableExtensions |
Sequence() |
IEnumerable<Option<T>> → Option<List<T>>, IEnumerable<Result<T,E>> → Result<List<T>,E> (short-circuits) |
EnumerableExtensions |
Partition() |
IEnumerable<Result<T,E>> → (List<T> Values, List<E> Errors) (collects all errors) |
OptionAsyncExtensions |
BindAsync / MapAsync / OrElseAsync |
Async Option chaining (Task & ValueTask) |
OptionAsyncExtensions |
BindAsync / MapAsync on Option<T> |
Start an async pipeline from a synchronous Option<T> — no Task.FromResult wrapper needed |
ResultAsyncExtensions |
BindAsync / MapAsync / MapErrAsync / OrElseAsync / TapAsync / TapErrAsync |
Async Result chaining (Task & ValueTask) |
ResultAsyncExtensions |
BindAsync / MapAsync on Result<T,E> |
Start an async pipeline from a synchronous Result<T,E> — short-circuits with zero allocation |
Security Considerations
Unwrap() and UnwrapErr() throw an InvalidOperationException whose default message embeds the
ToString() output of the value you didn't ask for (Unwrap() on Err includes the error; UnwrapErr()
on Ok includes the value):
var result = Result<int, string>.Err(secretValidationDetails);
result.Unwrap(); // message: "Result is Err: {secretValidationDetails}"
If T or E may carry sensitive data (raw user input, tokens, connection strings, PII), that content can
flow into logs, telemetry, or error responses through an uncaught exception. The same applies to ToString(),
which always renders Ok(value) / Err(error) for debugging purposes.
Guidance:
- Prefer
Expect(message)/ExpectErr(message)with a static, non-sensitive message when the value/error might be sensitive — the message is entirely caller-controlled. - Prefer
Match/TryGetOk/TryGetErrwhen you need to branch on success/failure without ever risking the default exception path. - Avoid putting secrets directly in
E(orT); wrap them in an error/DTO type whoseToString()you control, or scrub sensitive fields before they reach aResult/Option.
Project Structure
Rayleigh/
├── src/
│ └── jIAnSoft.Rayleigh/ # Core library
├── tests/
│ └── jIAnSoft.Rayleigh.Tests/ # Unit tests (xUnit)
├── examples/
│ └── jIAnSoft.Rayleigh.Examples/ # Runnable tutorial (E01–E12, heavily commented)
├── benchmarks/
│ └── jIAnSoft.Rayleigh.Benchmarks/ # BenchmarkDotNet allocation/throughput benchmarks
├── CHANGELOG.md # Release notes
├── LICENSE
└── README.md
Learn by Running
The examples/ project is a runnable tutorial, not a code dump. Each of the twelve modules explains
why an API exists before showing how to use it, then prints every expression next to its actual result:
# Walk through all twelve modules
dotnet run --project examples/jIAnSoft.Rayleigh.Examples
# Or jump to one — e.g. module 8, collection operations
dotnet run --project examples/jIAnSoft.Rayleigh.Examples -- 8
| # | Module | Covers |
|---|---|---|
| E01 | Option basics | What Option is, four ways to create one, IsSome / Contains |
| E02 | Option transformations | Map / Filter / Bind / Flatten — and how to pick between them |
| E03 | Getting values out | Match, TryGetValue, the Unwrap family, Or, Tap, Zip |
| E04 | Result basics | Carrying a failure reason; choosing an error type; the poisoned default |
| E05 | Result transformations | Map / MapErr / Bind and railway-oriented programming |
| E06 | Result extraction | Match, TryGetOk, fallbacks, logging, the Unit type |
| E07 | Option ↔ Result | Which one to reach for, and how to convert between them |
| E08 | Collections | FirstOrNone, GetValueOrNone, Sequence, Partition, Values |
| E09 | Async pipelines | BindAsync / MapAsync, zero-allocation short-circuit, cancellation |
| E10 | LINQ query syntax | from / where / select over Option and Result |
| E11 | Real-world scenarios | Config, form validation, sign-up, batch import, layered cache |
| E12 | Common pitfalls | Ten mistakes worth seeing once, each with the fix |
Modules are written for someone meeting
Option/Resultfor the first time. If you already know the concepts, E08 and E12 are the ones with material you likely haven't seen.
Build
dotnet build
Test
dotnet test
Benchmarks
The benchmarks/ project uses BenchmarkDotNet to measure allocation and
throughput for Option<T> and Result<T, E>, substantiating the "zero-allocation" claim above and guarding
against regressions. Run it in Release mode (BenchmarkDotNet refuses to run under a Debug build):
dotnet run -c Release --project benchmarks/jIAnSoft.Rayleigh.Benchmarks
Run a single suite, or a quick pass with fewer iterations:
# One suite
dotnet run -c Release --project benchmarks/jIAnSoft.Rayleigh.Benchmarks -- --filter '*EnumerableBenchmarks*'
# Quick pass (lower precision, much faster)
dotnet run -c Release --project benchmarks/jIAnSoft.Rayleigh.Benchmarks -- --filter '*' --job short
| Suite | What it establishes |
|---|---|
OptionBenchmarks |
Option<T> allocates nothing on its own; the contrast group shows closure cost is the caller's, not the library's |
ResultBenchmarks |
Same for Result<T,E>; the uninitialized-state guard costs nothing on the success path |
EnumerableBenchmarks |
The collection combinators' stronger semantics don't cost throughput vs. their LINQ counterparts |
AsyncPipelineBenchmarks |
Sync-source overloads short-circuit with 0 B allocated, vs. the Task.FromResult wrapper they replace |
ThrowPathBenchmarks |
Quantifies why Result beats exception-driven control flow on the error path |
Representative numbers (BenchmarkDotNet 0.15.8, AMD Ryzen 9 5950X, .NET 10.0.9):
| Mean | Allocated | |
|---|---|---|
Result.Ok(v) |
0.13 ns | 0 B |
result.UnwrapOr(-1) on Err |
0.16 ns | 0 B |
try { result.Unwrap(); } catch { } — same error, via exception |
2,100 ns | 384 B |
err.BindAsync(...) — sync-source, short-circuit |
13.9 ns | 0 B |
Task.FromResult(err).BindAsync(...) — the wrapper it replaces |
38.4 ns | 240 B |
On precision: these come from a
--job shortrun (3 iterations), so theMeancolumn has wide error bars and is directional only. TheAllocatedcolumn is measured deterministically via GC counters and is exact. Re-run without--job shortfor citable timings.
License
This project is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
26.7.26
修正 (Bug Fix)
- [重要] Result<T, TE> 的「未初始化偵測」先前在 TE 為值型別(enum、struct)時完全失效。
舊實作以 `_error is null` 判斷,但 `where TE : notnull` 之下 `TE?` 僅是可為 null 的標註而非
Nullable<TE>,因此該條件會被 JIT 常數摺疊為 false。結果是 default(Result<User, UserError>)
會偽裝成合法的 Err(UserError.NotFound)(enum 預設值通常是有意義的成員),讓呼叫端拿到
看似正常的業務錯誤而無從追查。現改用明確的三態欄位,對所有 TE 一致生效。
此修正不改變 struct 大小(實測 Result<int, MyEnum> 仍為 12 bytes)。
- EnumerableExtensions.Values() 對 null 來源改為「立即」拋出 ArgumentNullException,
而非延遲到列舉時才拋出 NullReferenceException。
行為變更 (Behavior Change)
- default(Result<T, TE>).ToString() 由 "Err()" 改為 "Uninitialized"。
舊輸出在 TE 為 enum 時會顯示為具誤導性的 "Err(None)"。此為偵錯輔助輸出,
不建議作為程式邏輯的判斷依據。
- default(Result<T, TE>) 不再等於錯誤值恰好為 default(TE) 的合法 Err。
兩個未初始化的 Result 仍彼此相等,GetHashCode 亦維持一致。
新增 (New API)
- 集合入口:FirstOrNone、FirstOrNone(predicate)、SingleOrNone、ElementAtOrNone、GetValueOrNone。
相較 FirstOrDefault / GetValueOrDefault,這些方法對值型別同樣正確——
能區分「找不到」與「找到了一個恰好等於預設值的元素」。
- 組合子:Sequence(Option 版與 Result 版,遇第一個 None/Err 即短路)、
Partition(走訪全部並蒐集所有錯誤,適合一次回報所有驗證問題)。
- 非同步:新增以同步的 Result<T,TE> / Option<T> 本身為 this 的 BindAsync / MapAsync 多載,
讓「同步起點 → 非同步步驟」的管線不需再包一層 Task.FromResult。
短路路徑刻意不宣告為 async,回傳同步完成的 ValueTask,零配置。
效能與內部改善
- 所有 throw helper 加上 MethodImplOptions.NoInlining,避免冷路徑 IL 被內聯進
AggressiveInlining 的熱路徑方法而撐大體積、影響 JIT 內聯決策。
- Result.SelectMany 與全部非同步擴充改用單次 TryGetOk/TryGetValue,
消除同一個值被重複驗證與分支多達三次的情況。
- Option.Zip / ZipWith 消除對第二個運算元的重複狀態檢查。
其他
- Unit 補上非泛型 IComparable,與 Option / Result 保持一致。
- 例外訊息改用 typeof(...) 呈現完整泛型型別名稱(原本只顯示 "Option" / "Result")。
- 建置設定鎖定 LangVersion 12.0(net8.0 與 net10.0 的語言版本交集),
確保新語法在兩個目標框架上都會被即時驗證。