mirror of
https://github.com/chylex/.NET-Community-Toolkit.git
synced 2025-04-13 20:15:45 +02:00
Merge branch 'master' into lazyLoadingThreshold
This commit is contained in:
commit
9db13dfb4e
Microsoft.Toolkit.HighPerformance
Box{T}.cs
Buffers
ArrayPoolBufferWriter{T}.cs
Internals
ArrayMemoryManager{TFrom,TTo}.cs
MemoryOwner{T}.csSpanOwner{T}.csStringPool.csInterfaces
ProxyMemoryManager{TFrom,TTo}.csStringMemoryManager{TTo}.csExtensions
ArrayExtensions.1D.csArrayExtensions.2D.csArrayExtensions.3D.csMemoryExtensions.csObjectExtensions.csReadOnlyMemoryExtensions.csReadOnlySpanExtensions.csSpanExtensions.csStringExtensions.cs
Helpers/Internals
Memory
Microsoft.Toolkit.HighPerformance.csprojMicrosoft.Toolkit.Mvvm
ComponentModel
DependencyInjection
Input
Messaging
UnitTests
UnitTests.HighPerformance.NetCore
UnitTests.HighPerformance.Shared
Buffers
Extensions
UnitTests.Shared
@ -78,7 +78,7 @@ public static Box<T> GetFrom(object obj)
|
||||
ThrowInvalidCastExceptionForGetFrom();
|
||||
}
|
||||
|
||||
return Unsafe.As<Box<T>>(obj);
|
||||
return Unsafe.As<Box<T>>(obj)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,7 +94,7 @@ public static Box<T> GetFrom(object obj)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Box<T> DangerousGetFrom(object obj)
|
||||
{
|
||||
return Unsafe.As<Box<T>>(obj);
|
||||
return Unsafe.As<Box<T>>(obj)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -108,7 +108,7 @@ public static bool TryGetFrom(object obj, [NotNullWhen(true)] out Box<T>? box)
|
||||
{
|
||||
if (obj.GetType() == typeof(T))
|
||||
{
|
||||
box = Unsafe.As<Box<T>>(obj);
|
||||
box = Unsafe.As<Box<T>>(obj)!;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -145,7 +145,7 @@ public static implicit operator Box<T>(T value)
|
||||
// manually be implemented in the Box<T> type. For instance, boxing a float
|
||||
// and calling ToString() on it directly, on its boxed object or on a Box<T>
|
||||
// reference retrieved from it will produce the same result in all cases.
|
||||
return Unsafe.As<Box<T>>(value);
|
||||
return Unsafe.As<Box<T>>(value)!;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -10,6 +10,7 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Views;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
using Microsoft.Toolkit.HighPerformance.Helpers.Internals;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Buffers
|
||||
{
|
||||
@ -233,7 +234,7 @@ public void Advance(int count)
|
||||
/// <inheritdoc/>
|
||||
public Memory<T> GetMemory(int sizeHint = 0)
|
||||
{
|
||||
CheckAndResizeBuffer(sizeHint);
|
||||
CheckBufferAndEnsureCapacity(sizeHint);
|
||||
|
||||
return this.array.AsMemory(this.index);
|
||||
}
|
||||
@ -241,7 +242,7 @@ public Memory<T> GetMemory(int sizeHint = 0)
|
||||
/// <inheritdoc/>
|
||||
public Span<T> GetSpan(int sizeHint = 0)
|
||||
{
|
||||
CheckAndResizeBuffer(sizeHint);
|
||||
CheckBufferAndEnsureCapacity(sizeHint);
|
||||
|
||||
return this.array.AsSpan(this.index);
|
||||
}
|
||||
@ -251,9 +252,11 @@ public Span<T> GetSpan(int sizeHint = 0)
|
||||
/// </summary>
|
||||
/// <param name="sizeHint">The minimum number of items to ensure space for in <see cref="array"/>.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void CheckAndResizeBuffer(int sizeHint)
|
||||
private void CheckBufferAndEnsureCapacity(int sizeHint)
|
||||
{
|
||||
if (this.array is null)
|
||||
T[]? array = this.array;
|
||||
|
||||
if (array is null)
|
||||
{
|
||||
ThrowObjectDisposedException();
|
||||
}
|
||||
@ -268,14 +271,34 @@ private void CheckAndResizeBuffer(int sizeHint)
|
||||
sizeHint = 1;
|
||||
}
|
||||
|
||||
if (sizeHint > FreeCapacity)
|
||||
if (sizeHint > array!.Length - this.index)
|
||||
{
|
||||
int minimumSize = this.index + sizeHint;
|
||||
|
||||
this.pool.Resize(ref this.array, minimumSize);
|
||||
ResizeBuffer(sizeHint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes <see cref="array"/> to ensure it can fit the specified number of new items.
|
||||
/// </summary>
|
||||
/// <param name="sizeHint">The minimum number of items to ensure space for in <see cref="array"/>.</param>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void ResizeBuffer(int sizeHint)
|
||||
{
|
||||
int minimumSize = this.index + sizeHint;
|
||||
|
||||
// The ArrayPool<T> class has a maximum threshold of 1024 * 1024 for the maximum length of
|
||||
// pooled arrays, and once this is exceeded it will just allocate a new array every time
|
||||
// of exactly the requested size. In that case, we manually round up the requested size to
|
||||
// the nearest power of two, to ensure that repeated consecutive writes when the array in
|
||||
// use is bigger than that threshold don't end up causing a resize every single time.
|
||||
if (minimumSize > 1024 * 1024)
|
||||
{
|
||||
minimumSize = BitOperations.RoundUpPowerOfTwo(minimumSize);
|
||||
}
|
||||
|
||||
this.pool.Resize(ref this.array, minimumSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
|
@ -0,0 +1,134 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom <see cref="MemoryManager{T}"/> that casts data from a <typeparamref name="TFrom"/> array, to <typeparamref name="TTo"/> values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">The source type of items to read.</typeparam>
|
||||
/// <typeparam name="TTo">The target type to cast the source items to.</typeparam>
|
||||
internal sealed class ArrayMemoryManager<TFrom, TTo> : MemoryManager<TTo>, IMemoryManager
|
||||
where TFrom : unmanaged
|
||||
where TTo : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// The source <typeparamref name="TFrom"/> array to read data from.
|
||||
/// </summary>
|
||||
private readonly TFrom[] array;
|
||||
|
||||
/// <summary>
|
||||
/// The starting offset within <see name="array"/>.
|
||||
/// </summary>
|
||||
private readonly int offset;
|
||||
|
||||
/// <summary>
|
||||
/// The original used length for <see name="array"/>.
|
||||
/// </summary>
|
||||
private readonly int length;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArrayMemoryManager{TFrom, TTo}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="array">The source <typeparamref name="TFrom"/> array to read data from.</param>
|
||||
/// <param name="offset">The starting offset within <paramref name="array"/>.</param>
|
||||
/// <param name="length">The original used length for <paramref name="array"/>.</param>
|
||||
public ArrayMemoryManager(TFrom[] array, int offset, int length)
|
||||
{
|
||||
this.array = array;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Span<TTo> GetSpan()
|
||||
{
|
||||
#if SPAN_RUNTIME_SUPPORT
|
||||
ref TFrom r0 = ref this.array.DangerousGetReferenceAt(this.offset);
|
||||
ref TTo r1 = ref Unsafe.As<TFrom, TTo>(ref r0);
|
||||
int length = RuntimeHelpers.ConvertLength<TFrom, TTo>(this.length);
|
||||
|
||||
return MemoryMarshal.CreateSpan(ref r1, length);
|
||||
#else
|
||||
Span<TFrom> span = this.array.AsSpan(this.offset, this.length);
|
||||
|
||||
// We rely on MemoryMarshal.Cast here to deal with calculating the effective
|
||||
// size of the new span to return. This will also make the behavior consistent
|
||||
// for users that are both using this type as well as casting spans directly.
|
||||
return MemoryMarshal.Cast<TFrom, TTo>(span);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override unsafe MemoryHandle Pin(int elementIndex = 0)
|
||||
{
|
||||
if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf<TFrom>() / Unsafe.SizeOf<TTo>()))
|
||||
{
|
||||
ThrowArgumentOutOfRangeExceptionForInvalidIndex();
|
||||
}
|
||||
|
||||
int
|
||||
bytePrefix = this.offset * Unsafe.SizeOf<TFrom>(),
|
||||
byteSuffix = elementIndex * Unsafe.SizeOf<TTo>(),
|
||||
byteOffset = bytePrefix + byteSuffix;
|
||||
|
||||
GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned);
|
||||
|
||||
ref TFrom r0 = ref this.array.DangerousGetReference();
|
||||
ref byte r1 = ref Unsafe.As<TFrom, byte>(ref r0);
|
||||
ref byte r2 = ref Unsafe.Add(ref r1, byteOffset);
|
||||
void* pi = Unsafe.AsPointer(ref r2);
|
||||
|
||||
return new MemoryHandle(pi, handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Unpin()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Memory<T> GetMemory<T>(int offset, int length)
|
||||
where T : unmanaged
|
||||
{
|
||||
// We need to calculate the right offset and length of the new Memory<T>. The local offset
|
||||
// is the original offset into the wrapped TFrom[] array, while the input offset is the one
|
||||
// with respect to TTo items in the Memory<TTo> instance that is currently being cast.
|
||||
int
|
||||
absoluteOffset = this.offset + RuntimeHelpers.ConvertLength<TTo, TFrom>(offset),
|
||||
absoluteLength = RuntimeHelpers.ConvertLength<TTo, TFrom>(length);
|
||||
|
||||
// We have a special handling in cases where the user is circling back to the original type
|
||||
// of the wrapped array. In this case we can just return a memory wrapping that array directly,
|
||||
// with offset and length being adjusted, without the memory manager indirection.
|
||||
if (typeof(T) == typeof(TFrom))
|
||||
{
|
||||
return (Memory<T>)(object)this.array.AsMemory(absoluteOffset, absoluteLength);
|
||||
}
|
||||
|
||||
return new ArrayMemoryManager<TFrom, T>(this.array, absoluteOffset, absoluteLength).Memory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the target index for <see cref="Pin"/> is invalid.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for a <see cref="MemoryManager{T}"/> instance that can reinterpret its underlying data.
|
||||
/// </summary>
|
||||
internal interface IMemoryManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Memory{T}"/> that reinterprets the underlying data for the current instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The target type to cast the items to.</typeparam>
|
||||
/// <param name="offset">The starting offset within the data store.</param>
|
||||
/// <param name="length">The original used length for the data store.</param>
|
||||
/// <returns>A new <see cref="Memory{T}"/> instance of the specified type, reinterpreting the current items.</returns>
|
||||
Memory<T> GetMemory<T>(int offset, int length)
|
||||
where T : unmanaged;
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
|
||||
using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom <see cref="MemoryManager{T}"/> that casts data from a <see cref="MemoryManager{T}"/> of <typeparamref name="TFrom"/>, to <typeparamref name="TTo"/> values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">The source type of items to read.</typeparam>
|
||||
/// <typeparam name="TTo">The target type to cast the source items to.</typeparam>
|
||||
internal sealed class ProxyMemoryManager<TFrom, TTo> : MemoryManager<TTo>, IMemoryManager
|
||||
where TFrom : unmanaged
|
||||
where TTo : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// The source <see cref="MemoryManager{T}"/> to read data from.
|
||||
/// </summary>
|
||||
private readonly MemoryManager<TFrom> memoryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The starting offset within <see name="memoryManager"/>.
|
||||
/// </summary>
|
||||
private readonly int offset;
|
||||
|
||||
/// <summary>
|
||||
/// The original used length for <see name="memoryManager"/>.
|
||||
/// </summary>
|
||||
private readonly int length;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProxyMemoryManager{TFrom, TTo}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="memoryManager">The source <see cref="MemoryManager{T}"/> to read data from.</param>
|
||||
/// <param name="offset">The starting offset within <paramref name="memoryManager"/>.</param>
|
||||
/// <param name="length">The original used length for <paramref name="memoryManager"/>.</param>
|
||||
public ProxyMemoryManager(MemoryManager<TFrom> memoryManager, int offset, int length)
|
||||
{
|
||||
this.memoryManager = memoryManager;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Span<TTo> GetSpan()
|
||||
{
|
||||
Span<TFrom> span = this.memoryManager.GetSpan().Slice(this.offset, this.length);
|
||||
|
||||
return MemoryMarshal.Cast<TFrom, TTo>(span);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MemoryHandle Pin(int elementIndex = 0)
|
||||
{
|
||||
if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf<TFrom>() / Unsafe.SizeOf<TTo>()))
|
||||
{
|
||||
ThrowArgumentExceptionForInvalidIndex();
|
||||
}
|
||||
|
||||
int
|
||||
bytePrefix = this.offset * Unsafe.SizeOf<TFrom>(),
|
||||
byteSuffix = elementIndex * Unsafe.SizeOf<TTo>(),
|
||||
byteOffset = bytePrefix + byteSuffix;
|
||||
|
||||
#if NETSTANDARD1_4
|
||||
int
|
||||
shiftedOffset = byteOffset / Unsafe.SizeOf<TFrom>(),
|
||||
remainder = byteOffset - (shiftedOffset * Unsafe.SizeOf<TFrom>());
|
||||
#else
|
||||
int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf<TFrom>(), out int remainder);
|
||||
#endif
|
||||
|
||||
if (remainder != 0)
|
||||
{
|
||||
ThrowArgumentExceptionForInvalidAlignment();
|
||||
}
|
||||
|
||||
return this.memoryManager.Pin(shiftedOffset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Unpin()
|
||||
{
|
||||
this.memoryManager.Unpin();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
((IDisposable)this.memoryManager).Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Memory<T> GetMemory<T>(int offset, int length)
|
||||
where T : unmanaged
|
||||
{
|
||||
// Like in the other memory manager, calculate the absolute offset and length
|
||||
int
|
||||
absoluteOffset = this.offset + RuntimeHelpers.ConvertLength<TTo, TFrom>(offset),
|
||||
absoluteLength = RuntimeHelpers.ConvertLength<TTo, TFrom>(length);
|
||||
|
||||
// Skip one indirection level and slice the original memory manager, if possible
|
||||
if (typeof(T) == typeof(TFrom))
|
||||
{
|
||||
return (Memory<T>)(object)this.memoryManager.Memory.Slice(absoluteOffset, absoluteLength);
|
||||
}
|
||||
|
||||
return new ProxyMemoryManager<TFrom, T>(this.memoryManager, absoluteOffset, absoluteLength).Memory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the target index for <see cref="Pin"/> is invalid.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentExceptionForInvalidIndex()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when <see cref="Pin"/> receives an invalid target index.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentExceptionForInvalidAlignment()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("elementIndex", "The input index doesn't result in an aligned item access");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom <see cref="MemoryManager{T}"/> that casts data from a <see cref="string"/> to <typeparamref name="TTo"/> values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TTo">The target type to cast the source characters to.</typeparam>
|
||||
internal sealed class StringMemoryManager<TTo> : MemoryManager<TTo>, IMemoryManager
|
||||
where TTo : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// The source <see cref="string"/> to read data from.
|
||||
/// </summary>
|
||||
private readonly string text;
|
||||
|
||||
/// <summary>
|
||||
/// The starting offset within <see name="array"/>.
|
||||
/// </summary>
|
||||
private readonly int offset;
|
||||
|
||||
/// <summary>
|
||||
/// The original used length for <see name="array"/>.
|
||||
/// </summary>
|
||||
private readonly int length;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringMemoryManager{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The source <see cref="string"/> to read data from.</param>
|
||||
/// <param name="offset">The starting offset within <paramref name="text"/>.</param>
|
||||
/// <param name="length">The original used length for <paramref name="text"/>.</param>
|
||||
public StringMemoryManager(string text, int offset, int length)
|
||||
{
|
||||
this.text = text;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Span<TTo> GetSpan()
|
||||
{
|
||||
#if SPAN_RUNTIME_SUPPORT
|
||||
ref char r0 = ref this.text.DangerousGetReferenceAt(this.offset);
|
||||
ref TTo r1 = ref Unsafe.As<char, TTo>(ref r0);
|
||||
int length = RuntimeHelpers.ConvertLength<char, TTo>(this.length);
|
||||
|
||||
return MemoryMarshal.CreateSpan(ref r1, length);
|
||||
#else
|
||||
ReadOnlyMemory<char> memory = this.text.AsMemory(this.offset, this.length);
|
||||
Span<char> span = MemoryMarshal.AsMemory(memory).Span;
|
||||
|
||||
return MemoryMarshal.Cast<char, TTo>(span);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override unsafe MemoryHandle Pin(int elementIndex = 0)
|
||||
{
|
||||
if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf<char>() / Unsafe.SizeOf<TTo>()))
|
||||
{
|
||||
ThrowArgumentOutOfRangeExceptionForInvalidIndex();
|
||||
}
|
||||
|
||||
int
|
||||
bytePrefix = this.offset * Unsafe.SizeOf<char>(),
|
||||
byteSuffix = elementIndex * Unsafe.SizeOf<TTo>(),
|
||||
byteOffset = bytePrefix + byteSuffix;
|
||||
|
||||
GCHandle handle = GCHandle.Alloc(this.text, GCHandleType.Pinned);
|
||||
|
||||
ref char r0 = ref this.text.DangerousGetReference();
|
||||
ref byte r1 = ref Unsafe.As<char, byte>(ref r0);
|
||||
ref byte r2 = ref Unsafe.Add(ref r1, byteOffset);
|
||||
void* pi = Unsafe.AsPointer(ref r2);
|
||||
|
||||
return new MemoryHandle(pi, handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Unpin()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Memory<T> GetMemory<T>(int offset, int length)
|
||||
where T : unmanaged
|
||||
{
|
||||
int
|
||||
absoluteOffset = this.offset + RuntimeHelpers.ConvertLength<TTo, char>(offset),
|
||||
absoluteLength = RuntimeHelpers.ConvertLength<TTo, char>(length);
|
||||
|
||||
if (typeof(T) == typeof(char))
|
||||
{
|
||||
ReadOnlyMemory<char> memory = this.text.AsMemory(absoluteOffset, absoluteLength);
|
||||
|
||||
return (Memory<T>)(object)MemoryMarshal.AsMemory(memory);
|
||||
}
|
||||
|
||||
return new StringMemoryManager<T>(this.text, absoluteOffset, absoluteLength).Memory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the target index for <see cref="Pin"/> is invalid.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,9 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
#if NETCORE_RUNTIME
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Views;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
|
||||
@ -180,7 +183,22 @@ public Span<T> Span
|
||||
ThrowObjectDisposedException();
|
||||
}
|
||||
|
||||
#if NETCORE_RUNTIME
|
||||
ref T r0 = ref array!.DangerousGetReferenceAt(this.start);
|
||||
|
||||
// On .NET Core runtimes, we can manually create a span from the starting reference to
|
||||
// skip the argument validations, which include an explicit null check, covariance check
|
||||
// for the array and the actual validation for the starting offset and target length. We
|
||||
// only do this on .NET Core as we can leverage the runtime-specific array layout to get
|
||||
// a fast access to the initial element, which makes this trick worth it. Otherwise, on
|
||||
// runtimes where we would need to at least access a static field to retrieve the base
|
||||
// byte offset within an SZ array object, we can get better performance by just using the
|
||||
// default Span<T> constructor and paying the cost of the extra conditional branches,
|
||||
// especially if T is a value type, in which case the covariance check is JIT removed.
|
||||
return MemoryMarshal.CreateSpan(ref r0, this.length);
|
||||
#else
|
||||
return new Span<T>(array!, this.start, this.length);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,6 +226,31 @@ public ref T DangerousGetReference()
|
||||
return ref array!.DangerousGetReferenceAt(this.start);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.</returns>
|
||||
/// <exception cref="ObjectDisposedException">Thrown when the buffer in use has already been disposed.</exception>
|
||||
/// <remarks>
|
||||
/// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution.
|
||||
/// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's
|
||||
/// not used after the current <see cref="MemoryOwner{T}"/> instance is disposed. Doing so is considered undefined behavior,
|
||||
/// as the same array might be in use within another <see cref="MemoryOwner{T}"/> instance.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ArraySegment<T> DangerousGetArray()
|
||||
{
|
||||
T[]? array = this.array;
|
||||
|
||||
if (array is null)
|
||||
{
|
||||
ThrowObjectDisposedException();
|
||||
}
|
||||
|
||||
return new ArraySegment<T>(array!, this.start, this.length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slices the buffer currently in use and returns a new <see cref="MemoryOwner{T}"/> instance.
|
||||
/// </summary>
|
||||
@ -222,7 +265,6 @@ public ref T DangerousGetReference()
|
||||
/// size and copy the previous items into the new one, or needing an additional variable/field
|
||||
/// to manually handle to track the used range within a given <see cref="MemoryOwner{T}"/> instance.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
public MemoryOwner<T> Slice(int start, int length)
|
||||
{
|
||||
T[]? array = this.array;
|
||||
@ -244,6 +286,11 @@ public MemoryOwner<T> Slice(int start, int length)
|
||||
ThrowInvalidLengthException();
|
||||
}
|
||||
|
||||
// We're transferring the ownership of the underlying array, so the current
|
||||
// instance no longer needs to be disposed. Because of this, we can manually
|
||||
// suppress the finalizer to reduce the overhead on the garbage collector.
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
return new MemoryOwner<T>(start, length, this.pool, array!);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,9 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
#if NETCORE_RUNTIME
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Views;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
|
||||
@ -143,7 +146,16 @@ public int Length
|
||||
public Span<T> Span
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new Span<T>(this.array, 0, this.length);
|
||||
get
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
ref T r0 = ref array!.DangerousGetReference();
|
||||
|
||||
return MemoryMarshal.CreateSpan(ref r0, this.length);
|
||||
#else
|
||||
return new Span<T>(this.array, 0, this.length);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -157,6 +169,23 @@ public ref T DangerousGetReference()
|
||||
return ref this.array.DangerousGetReference();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="ArraySegment{T}"/> instance wrapping the underlying <typeparamref name="T"/> array in use.</returns>
|
||||
/// <remarks>
|
||||
/// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution.
|
||||
/// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's
|
||||
/// not used after the current <see cref="SpanOwner{T}"/> instance is disposed. Doing so is considered undefined behavior,
|
||||
/// as the same array might be in use within another <see cref="SpanOwner{T}"/> instance.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ArraySegment<T> DangerousGetArray()
|
||||
{
|
||||
return new ArraySegment<T>(array!, 0, this.length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the duck-typed <see cref="IDisposable.Dispose"/> method.
|
||||
/// </summary>
|
||||
|
@ -5,15 +5,13 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
#if NETCOREAPP3_1
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
#if !NETSTANDARD1_4
|
||||
using Microsoft.Toolkit.HighPerformance.Helpers;
|
||||
#endif
|
||||
using BitOperations = Microsoft.Toolkit.HighPerformance.Helpers.Internals.BitOperations;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -79,8 +77,8 @@ static void FindFactors(int size, int factor, out int x, out int y)
|
||||
a = Math.Sqrt((double)size / factor),
|
||||
b = factor * a;
|
||||
|
||||
x = RoundUpPowerOfTwo((int)a);
|
||||
y = RoundUpPowerOfTwo((int)b);
|
||||
x = BitOperations.RoundUpPowerOfTwo((int)a);
|
||||
y = BitOperations.RoundUpPowerOfTwo((int)b);
|
||||
}
|
||||
|
||||
// We want to find two powers of 2 factors that produce a number
|
||||
@ -130,30 +128,6 @@ static void FindFactors(int size, int factor, out int x, out int y)
|
||||
Size = p2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds up an <see cref="int"/> value to a power of 2.
|
||||
/// </summary>
|
||||
/// <param name="x">The input value to round up.</param>
|
||||
/// <returns>The smallest power of two greater than or equal to <paramref name="x"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int RoundUpPowerOfTwo(int x)
|
||||
{
|
||||
#if NETCOREAPP3_1
|
||||
return 1 << (32 - BitOperations.LeadingZeroCount((uint)(x - 1)));
|
||||
#else
|
||||
x--;
|
||||
x |= x >> 1;
|
||||
x |= x >> 2;
|
||||
x |= x >> 4;
|
||||
x |= x >> 8;
|
||||
x |= x >> 16;
|
||||
x++;
|
||||
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared <see cref="StringPool"/> instance.
|
||||
/// </summary>
|
||||
@ -422,11 +396,11 @@ public object SyncRoot
|
||||
/// <param name="value">The input <see cref="string"/> instance to cache.</param>
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void Add(string value, int hashcode)
|
||||
public void Add(string value, int hashcode)
|
||||
{
|
||||
ref string target = ref TryGet(value.AsSpan(), hashcode);
|
||||
|
||||
if (Unsafe.AreSame(ref target, ref Unsafe.AsRef<string>(null)))
|
||||
if (Unsafe.IsNullRef(ref target))
|
||||
{
|
||||
Insert(value, hashcode);
|
||||
}
|
||||
@ -443,11 +417,11 @@ public unsafe void Add(string value, int hashcode)
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
|
||||
/// <returns>A <see cref="string"/> instance with the contents of <paramref name="value"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe string GetOrAdd(string value, int hashcode)
|
||||
public string GetOrAdd(string value, int hashcode)
|
||||
{
|
||||
ref string result = ref TryGet(value.AsSpan(), hashcode);
|
||||
|
||||
if (!Unsafe.AreSame(ref result, ref Unsafe.AsRef<string>(null)))
|
||||
if (!Unsafe.IsNullRef(ref result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@ -464,11 +438,11 @@ public unsafe string GetOrAdd(string value, int hashcode)
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="span"/>.</param>
|
||||
/// <returns>A <see cref="string"/> instance with the contents of <paramref name="span"/>, cached if possible.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe string GetOrAdd(ReadOnlySpan<char> span, int hashcode)
|
||||
public string GetOrAdd(ReadOnlySpan<char> span, int hashcode)
|
||||
{
|
||||
ref string result = ref TryGet(span, hashcode);
|
||||
|
||||
if (!Unsafe.AreSame(ref result, ref Unsafe.AsRef<string>(null)))
|
||||
if (!Unsafe.IsNullRef(ref result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@ -488,11 +462,11 @@ public unsafe string GetOrAdd(ReadOnlySpan<char> span, int hashcode)
|
||||
/// <param name="value">The resulting cached <see cref="string"/> instance, if present</param>
|
||||
/// <returns>Whether or not the target <see cref="string"/> instance was found.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe bool TryGet(ReadOnlySpan<char> span, int hashcode, [NotNullWhen(true)] out string? value)
|
||||
public bool TryGet(ReadOnlySpan<char> span, int hashcode, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
ref string result = ref TryGet(span, hashcode);
|
||||
|
||||
if (!Unsafe.AreSame(ref result, ref Unsafe.AsRef<string>(null)))
|
||||
if (!Unsafe.IsNullRef(ref result))
|
||||
{
|
||||
value = result;
|
||||
|
||||
@ -527,7 +501,7 @@ public void Reset()
|
||||
private unsafe ref string TryGet(ReadOnlySpan<char> span, int hashcode)
|
||||
{
|
||||
ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
|
||||
ref MapEntry entry = ref Unsafe.AsRef<MapEntry>(null);
|
||||
ref MapEntry entry = ref Unsafe.NullRef<MapEntry>();
|
||||
int
|
||||
length = this.buckets.Length,
|
||||
bucketIndex = hashcode & (length - 1);
|
||||
@ -547,7 +521,7 @@ private unsafe ref string TryGet(ReadOnlySpan<char> span, int hashcode)
|
||||
}
|
||||
}
|
||||
|
||||
return ref Unsafe.AsRef<string>(null);
|
||||
return ref Unsafe.NullRef<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -31,7 +31,7 @@ public static partial class ArrayExtensions
|
||||
public static ref T DangerousGetReference<T>(this T[] array)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArrayData>(array);
|
||||
var arrayData = Unsafe.As<RawArrayData>(array)!;
|
||||
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
|
||||
|
||||
return ref r0;
|
||||
@ -55,7 +55,7 @@ public static ref T DangerousGetReference<T>(this T[] array)
|
||||
public static ref T DangerousGetReferenceAt<T>(this T[] array, int i)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArrayData>(array);
|
||||
var arrayData = Unsafe.As<RawArrayData>(array)!;
|
||||
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
|
||||
ref T ri = ref Unsafe.Add(ref r0, (nint)(uint)i);
|
||||
|
||||
|
@ -33,7 +33,7 @@ public static partial class ArrayExtensions
|
||||
public static ref T DangerousGetReference<T>(this T[,] array)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArray2DData>(array);
|
||||
var arrayData = Unsafe.As<RawArray2DData>(array)!;
|
||||
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
|
||||
|
||||
return ref r0;
|
||||
@ -63,7 +63,7 @@ public static ref T DangerousGetReference<T>(this T[,] array)
|
||||
public static ref T DangerousGetReferenceAt<T>(this T[,] array, int i, int j)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArray2DData>(array);
|
||||
var arrayData = Unsafe.As<RawArray2DData>(array)!;
|
||||
nint offset = ((nint)(uint)i * (nint)(uint)arrayData.Width) + (nint)(uint)j;
|
||||
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
|
||||
ref T ri = ref Unsafe.Add(ref r0, offset);
|
||||
|
@ -32,7 +32,7 @@ public static partial class ArrayExtensions
|
||||
public static ref T DangerousGetReference<T>(this T[,,] array)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArray3DData>(array);
|
||||
var arrayData = Unsafe.As<RawArray3DData>(array)!;
|
||||
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
|
||||
|
||||
return ref r0;
|
||||
@ -63,7 +63,7 @@ public static ref T DangerousGetReference<T>(this T[,,] array)
|
||||
public static ref T DangerousGetReferenceAt<T>(this T[,,] array, int i, int j, int k)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArray3DData>(array);
|
||||
var arrayData = Unsafe.As<RawArray3DData>(array)!;
|
||||
nint offset =
|
||||
((nint)(uint)i * (nint)(uint)arrayData.Height * (nint)(uint)arrayData.Width) +
|
||||
((nint)(uint)j * (nint)(uint)arrayData.Width) + (nint)(uint)k;
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
#if SPAN_RUNTIME_SUPPORT
|
||||
using Microsoft.Toolkit.HighPerformance.Memory;
|
||||
#endif
|
||||
@ -64,6 +65,41 @@ public static Memory2D<T> AsMemory2D<T>(this Memory<T> memory, int offset, int h
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="Memory{T}"/> of one primitive type <typeparamref name="T"/> to <see cref="Memory{T}"/> of bytes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type if items in the source <see cref="Memory{T}"/>.</typeparam>
|
||||
/// <param name="memory">The source <see cref="Memory{T}"/>, of type <typeparamref name="T"/>.</param>
|
||||
/// <returns>A <see cref="Memory{T}"/> of bytes.</returns>
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the <see cref="Memory{T}.Length"/> property of the new <see cref="Memory{T}"/> would exceed <see cref="int.MaxValue"/>.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the data store of <paramref name="memory"/> is not supported.</exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Memory<byte> AsBytes<T>(this Memory<T> memory)
|
||||
where T : unmanaged
|
||||
{
|
||||
return MemoryMarshal.AsMemory(((ReadOnlyMemory<T>)memory).Cast<T, byte>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="Memory{T}"/> of one primitive type <typeparamref name="TFrom"/> to another primitive type <typeparamref name="TTo"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">The type of items in the source <see cref="Memory{T}"/>.</typeparam>
|
||||
/// <typeparam name="TTo">The type of items in the destination <see cref="Memory{T}"/>.</typeparam>
|
||||
/// <param name="memory">The source <see cref="Memory{T}"/>, of type <typeparamref name="TFrom"/>.</param>
|
||||
/// <returns>A <see cref="Memory{T}"/> of type <typeparamref name="TTo"/></returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the data store of <paramref name="memory"/> is not supported.</exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Memory<TTo> Cast<TFrom, TTo>(this Memory<TFrom> memory)
|
||||
where TFrom : unmanaged
|
||||
where TTo : unmanaged
|
||||
{
|
||||
return MemoryMarshal.AsMemory(((ReadOnlyMemory<TFrom>)memory).Cast<TFrom, TTo>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="Stream"/> wrapping the contents of the given <see cref="Memory{T}"/> of <see cref="byte"/> instance.
|
||||
/// </summary>
|
||||
|
@ -31,7 +31,7 @@ public static class ObjectExtensions
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IntPtr DangerousGetObjectDataByteOffset<T>(this object obj, ref T data)
|
||||
{
|
||||
var rawObj = Unsafe.As<RawObjectData>(obj);
|
||||
var rawObj = Unsafe.As<RawObjectData>(obj)!;
|
||||
ref byte r0 = ref rawObj.Data;
|
||||
ref byte r1 = ref Unsafe.As<T, byte>(ref data);
|
||||
|
||||
@ -55,7 +55,7 @@ public static IntPtr DangerousGetObjectDataByteOffset<T>(this object obj, ref T
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T DangerousGetObjectDataReferenceAt<T>(this object obj, IntPtr offset)
|
||||
{
|
||||
var rawObj = Unsafe.As<RawObjectData>(obj);
|
||||
var rawObj = Unsafe.As<RawObjectData>(obj)!;
|
||||
ref byte r0 = ref rawObj.Data;
|
||||
ref byte r1 = ref Unsafe.AddByteOffset(ref r0, offset);
|
||||
ref T r2 = ref Unsafe.As<byte, T>(ref r1);
|
||||
|
@ -3,9 +3,13 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Internals;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
|
||||
#if SPAN_RUNTIME_SUPPORT
|
||||
using Microsoft.Toolkit.HighPerformance.Memory;
|
||||
#endif
|
||||
@ -64,6 +68,77 @@ public static ReadOnlyMemory2D<T> AsMemory2D<T>(this ReadOnlyMemory<T> memory, i
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="ReadOnlyMemory{T}"/> of one primitive type <typeparamref name="T"/> to <see cref="ReadOnlyMemory{T}"/> of bytes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type if items in the source <see cref="ReadOnlyMemory{T}"/>.</typeparam>
|
||||
/// <param name="memory">The source <see cref="ReadOnlyMemory{T}"/>, of type <typeparamref name="T"/>.</param>
|
||||
/// <returns>A <see cref="ReadOnlyMemory{T}"/> of bytes.</returns>
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the <see cref="ReadOnlyMemory{T}.Length"/> property of the new <see cref="ReadOnlyMemory{T}"/> would exceed <see cref="int.MaxValue"/>.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the data store of <paramref name="memory"/> is not supported.</exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ReadOnlyMemory<byte> AsBytes<T>(this ReadOnlyMemory<T> memory)
|
||||
where T : unmanaged
|
||||
{
|
||||
return Cast<T, byte>(memory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="ReadOnlyMemory{T}"/> of one primitive type <typeparamref name="TFrom"/> to another primitive type <typeparamref name="TTo"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">The type of items in the source <see cref="ReadOnlyMemory{T}"/>.</typeparam>
|
||||
/// <typeparam name="TTo">The type of items in the destination <see cref="ReadOnlyMemory{T}"/>.</typeparam>
|
||||
/// <param name="memory">The source <see cref="ReadOnlyMemory{T}"/>, of type <typeparamref name="TFrom"/>.</param>
|
||||
/// <returns>A <see cref="ReadOnlyMemory{T}"/> of type <typeparamref name="TTo"/></returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the data store of <paramref name="memory"/> is not supported.</exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ReadOnlyMemory<TTo> Cast<TFrom, TTo>(this ReadOnlyMemory<TFrom> memory)
|
||||
where TFrom : unmanaged
|
||||
where TTo : unmanaged
|
||||
{
|
||||
if (memory.IsEmpty)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typeof(TFrom) == typeof(char) &&
|
||||
MemoryMarshal.TryGetString((ReadOnlyMemory<char>)(object)memory, out string? text, out int start, out int length))
|
||||
{
|
||||
return new StringMemoryManager<TTo>(text!, start, length).Memory;
|
||||
}
|
||||
|
||||
if (MemoryMarshal.TryGetArray(memory, out ArraySegment<TFrom> segment))
|
||||
{
|
||||
return new ArrayMemoryManager<TFrom, TTo>(segment.Array!, segment.Offset, segment.Count).Memory;
|
||||
}
|
||||
|
||||
if (MemoryMarshal.TryGetMemoryManager<TFrom, MemoryManager<TFrom>>(memory, out var memoryManager, out start, out length))
|
||||
{
|
||||
// If the memory manager is the one resulting from a previous cast, we can use it directly to retrieve
|
||||
// a new manager for the target type that wraps the original data store, instead of creating one that
|
||||
// wraps the current manager. This ensures that doing repeated casts always results in only up to one
|
||||
// indirection level in the chain of memory managers needed to access the target data buffer to use.
|
||||
if (memoryManager is IMemoryManager wrappingManager)
|
||||
{
|
||||
return wrappingManager.GetMemory<TTo>(start, length);
|
||||
}
|
||||
|
||||
return new ProxyMemoryManager<TFrom, TTo>(memoryManager, start, length).Memory;
|
||||
}
|
||||
|
||||
// Throws when the memory instance has an unsupported backing store
|
||||
static ReadOnlyMemory<TTo> ThrowArgumentExceptionForUnsupportedMemory()
|
||||
{
|
||||
throw new ArgumentException("The input instance doesn't have a supported underlying data store.");
|
||||
}
|
||||
|
||||
return ThrowArgumentExceptionForUnsupportedMemory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="Stream"/> wrapping the contents of the given <see cref="ReadOnlyMemory{T}"/> of <see cref="byte"/> instance.
|
||||
/// </summary>
|
||||
|
@ -259,14 +259,10 @@ public static int Count<T>(this ReadOnlySpan<T> span, T value)
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="ReadOnlySpan{T}"/> of one primitive type <typeparamref name="T"/> to <see cref="ReadOnlySpan{T}"/> of bytes.
|
||||
/// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type if items in the source <see cref="ReadOnlySpan{T}"/>.</typeparam>
|
||||
/// <param name="span">The source slice, of type <typeparamref name="T"/>.</param>
|
||||
/// <returns>A <see cref="ReadOnlySpan{T}"/> of bytes.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <typeparamref name="T"/> contains pointers.
|
||||
/// </exception>
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the <see cref="ReadOnlySpan{T}.Length"/> property of the new <see cref="ReadOnlySpan{T}"/> would exceed <see cref="int.MaxValue"/>.
|
||||
/// </exception>
|
||||
@ -280,7 +276,6 @@ public static ReadOnlySpan<byte> AsBytes<T>(this ReadOnlySpan<T> span)
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="ReadOnlySpan{T}"/> of one primitive type <typeparamref name="TFrom"/> to another primitive type <typeparamref name="TTo"/>.
|
||||
/// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">The type of items in the source <see cref="ReadOnlySpan{T}"/>.</typeparam>
|
||||
/// <typeparam name="TTo">The type of items in the destination <see cref="ReadOnlySpan{T}"/>.</typeparam>
|
||||
@ -289,14 +284,11 @@ public static ReadOnlySpan<byte> AsBytes<T>(this ReadOnlySpan<T> span)
|
||||
/// <remarks>
|
||||
/// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <typeparamref name="TFrom"/> or <typeparamref name="TTo"/> contains pointers.
|
||||
/// </exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(this ReadOnlySpan<TFrom> span)
|
||||
where TFrom : struct
|
||||
where TTo : struct
|
||||
where TFrom : unmanaged
|
||||
where TTo : unmanaged
|
||||
{
|
||||
return MemoryMarshal.Cast<TFrom, TTo>(span);
|
||||
}
|
||||
|
@ -117,14 +117,10 @@ public static Span2D<T> AsSpan2D<T>(this Span<T> span, int offset, int height, i
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="Span{T}"/> of one primitive type <typeparamref name="T"/> to <see cref="Span{T}"/> of bytes.
|
||||
/// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type if items in the source <see cref="Span{T}"/>.</typeparam>
|
||||
/// <param name="span">The source slice, of type <typeparamref name="T"/>.</param>
|
||||
/// <returns>A <see cref="Span{T}"/> of bytes.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <typeparamref name="T"/> contains pointers.
|
||||
/// </exception>
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the <see cref="Span{T}.Length"/> property of the new <see cref="Span{T}"/> would exceed <see cref="int.MaxValue"/>.
|
||||
/// </exception>
|
||||
@ -138,7 +134,6 @@ public static Span<byte> AsBytes<T>(this Span<T> span)
|
||||
|
||||
/// <summary>
|
||||
/// Casts a <see cref="Span{T}"/> of one primitive type <typeparamref name="TFrom"/> to another primitive type <typeparamref name="TTo"/>.
|
||||
/// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">The type of items in the source <see cref="Span{T}"/>.</typeparam>
|
||||
/// <typeparam name="TTo">The type of items in the destination <see cref="Span{T}"/>.</typeparam>
|
||||
@ -147,14 +142,11 @@ public static Span<byte> AsBytes<T>(this Span<T> span)
|
||||
/// <remarks>
|
||||
/// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <typeparamref name="TFrom"/> or <typeparamref name="TTo"/> contains pointers.
|
||||
/// </exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Span<TTo> Cast<TFrom, TTo>(this Span<TFrom> span)
|
||||
where TFrom : struct
|
||||
where TTo : struct
|
||||
where TFrom : unmanaged
|
||||
where TTo : unmanaged
|
||||
{
|
||||
return MemoryMarshal.Cast<TFrom, TTo>(span);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ public static ref char DangerousGetReference(this string text)
|
||||
#if NETCOREAPP3_1
|
||||
return ref Unsafe.AsRef(text.GetPinnableReference());
|
||||
#elif NETCOREAPP2_1
|
||||
var stringData = Unsafe.As<RawStringData>(text);
|
||||
var stringData = Unsafe.As<RawStringData>(text)!;
|
||||
|
||||
return ref stringData.Data;
|
||||
#else
|
||||
@ -53,7 +53,7 @@ public static ref char DangerousGetReferenceAt(this string text, int i)
|
||||
#if NETCOREAPP3_1
|
||||
ref char r0 = ref Unsafe.AsRef(text.GetPinnableReference());
|
||||
#elif NETCOREAPP2_1
|
||||
ref char r0 = ref Unsafe.As<RawStringData>(text).Data;
|
||||
ref char r0 = ref Unsafe.As<RawStringData>(text)!.Data;
|
||||
#else
|
||||
ref char r0 = ref MemoryMarshal.GetReference(text.AsSpan());
|
||||
#endif
|
||||
|
@ -0,0 +1,43 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
#if NETCOREAPP3_1
|
||||
using static System.Numerics.BitOperations;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for intrinsic bit-twiddling operations. The methods use hardware intrinsics
|
||||
/// when available on the underlying platform, otherwise they use optimized software fallbacks.
|
||||
/// </summary>
|
||||
internal static class BitOperations
|
||||
{
|
||||
/// <summary>
|
||||
/// Rounds up an <see cref="int"/> value to a power of 2.
|
||||
/// </summary>
|
||||
/// <param name="x">The input value to round up.</param>
|
||||
/// <returns>The smallest power of two greater than or equal to <paramref name="x"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int RoundUpPowerOfTwo(int x)
|
||||
{
|
||||
#if NETCOREAPP3_1
|
||||
return 1 << (32 - LeadingZeroCount((uint)(x - 1)));
|
||||
#else
|
||||
x--;
|
||||
x |= x >> 1;
|
||||
x |= x >> 2;
|
||||
x |= x >> 4;
|
||||
x |= x >> 8;
|
||||
x |= x >> 16;
|
||||
x++;
|
||||
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@ -18,10 +18,40 @@
|
||||
namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class that act as polyfill for .NET Standard 2.0 and below.
|
||||
/// A helper class that with utility methods for dealing with references, and other low-level details.
|
||||
/// It also contains some APIs that act as polyfills for .NET Standard 2.0 and below.
|
||||
/// </summary>
|
||||
internal static class RuntimeHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a length of items from one size to another (rounding towards zero).
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">The source type of items.</typeparam>
|
||||
/// <typeparam name="TTo">The target type of items.</typeparam>
|
||||
/// <param name="length">The input length to convert.</param>
|
||||
/// <returns>The converted length for the specified argument and types.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe int ConvertLength<TFrom, TTo>(int length)
|
||||
where TFrom : unmanaged
|
||||
where TTo : unmanaged
|
||||
{
|
||||
if (sizeof(TFrom) == sizeof(TTo))
|
||||
{
|
||||
return length;
|
||||
}
|
||||
else if (sizeof(TFrom) == 1)
|
||||
{
|
||||
return length / sizeof(TTo);
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong targetLength = (ulong)(uint)length * (uint)sizeof(TFrom) / (uint)sizeof(TTo);
|
||||
|
||||
return checked((int)targetLength);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of a given array as a native integer.
|
||||
/// </summary>
|
||||
|
@ -772,7 +772,7 @@ public bool TryGetMemory(out Memory<T> memory)
|
||||
}
|
||||
else if (typeof(T) == typeof(char) && this.instance.GetType() == typeof(string))
|
||||
{
|
||||
string text = Unsafe.As<string>(this.instance);
|
||||
string text = Unsafe.As<string>(this.instance)!;
|
||||
int index = text.AsSpan().IndexOf(in text.DangerousGetObjectDataReferenceAt<char>(this.offset));
|
||||
ReadOnlyMemory<char> temp = text.AsMemory(index, (int)Length);
|
||||
|
||||
@ -786,16 +786,13 @@ public bool TryGetMemory(out Memory<T> memory)
|
||||
}
|
||||
else if (this.instance is MemoryManager<T> memoryManager)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
// If the object is a MemoryManager<T>, just slice it as needed
|
||||
memory = memoryManager.Memory.Slice((int)(void*)this.offset, this.height * this.width);
|
||||
}
|
||||
// If the object is a MemoryManager<T>, just slice it as needed
|
||||
memory = memoryManager.Memory.Slice((int)(nint)this.offset, this.height * this.width);
|
||||
}
|
||||
else if (this.instance.GetType() == typeof(T[]))
|
||||
{
|
||||
// If it's a T[] array, also handle the initial offset
|
||||
T[] array = Unsafe.As<T[]>(this.instance);
|
||||
T[] array = Unsafe.As<T[]>(this.instance)!;
|
||||
int index = array.AsSpan().IndexOf(ref array.DangerousGetObjectDataReferenceAt<T>(this.offset));
|
||||
|
||||
memory = array.AsMemory(index, this.height * this.width);
|
||||
|
@ -794,7 +794,7 @@ public bool TryGetMemory(out ReadOnlyMemory<T> memory)
|
||||
// difference between the start of the Span<char> (which directly wraps just the actual character data
|
||||
// within the string), and the input reference, which we can get from the byte offset in use. The result
|
||||
// is the character index which we can use to create the final Memory<char> instance.
|
||||
string text = Unsafe.As<string>(this.instance);
|
||||
string text = Unsafe.As<string>(this.instance)!;
|
||||
int index = text.AsSpan().IndexOf(in text.DangerousGetObjectDataReferenceAt<char>(this.offset));
|
||||
ReadOnlyMemory<char> temp = text.AsMemory(index, (int)Length);
|
||||
|
||||
@ -802,16 +802,13 @@ public bool TryGetMemory(out ReadOnlyMemory<T> memory)
|
||||
}
|
||||
else if (this.instance is MemoryManager<T> memoryManager)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
// If the object is a MemoryManager<T>, just slice it as needed
|
||||
memory = memoryManager.Memory.Slice((int)(void*)this.offset, this.height * this.width);
|
||||
}
|
||||
// If the object is a MemoryManager<T>, just slice it as needed
|
||||
memory = memoryManager.Memory.Slice((int)(nint)this.offset, this.height * this.width);
|
||||
}
|
||||
else if (this.instance.GetType() == typeof(T[]))
|
||||
{
|
||||
// If it's a T[] array, also handle the initial offset
|
||||
T[] array = Unsafe.As<T[]>(this.instance);
|
||||
T[] array = Unsafe.As<T[]>(this.instance)!;
|
||||
int index = array.AsSpan().IndexOf(ref array.DangerousGetObjectDataReferenceAt<T>(this.offset));
|
||||
|
||||
memory = array.AsMemory(index, this.height * this.width);
|
||||
|
@ -41,7 +41,7 @@
|
||||
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
|
||||
<PackageReference Include="System.Threading.Tasks.Parallel" Version="4.3.0" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<When Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
@ -51,14 +51,14 @@
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.0" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.4" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<When Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
|
||||
<ItemGroup>
|
||||
|
||||
<!-- .NET Standard 2.1 doesn't have the Unsafe type -->
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
|
||||
@ -76,6 +76,9 @@
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
|
||||
<!-- NETCORE_RUNTIME: to avoid issues with APIs that assume a specific memory layout, we define a
|
||||
@ -89,7 +92,7 @@
|
||||
</When>
|
||||
<When Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<DefineConstants>SPAN_RUNTIME_SUPPORT;NETCORE_RUNTIME</DefineConstants>
|
||||
|
@ -27,25 +27,39 @@ public abstract class ObservableObject : INotifyPropertyChanged, INotifyProperty
|
||||
public event PropertyChangingEventHandler? PropertyChanging;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the required configuration when a property has changed, and then
|
||||
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
|
||||
/// Raises the <see cref="PropertyChanged"/> event.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <remarks>The base implementation only raises the <see cref="PropertyChanged"/> event.</remarks>
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
/// <param name="e">The input <see cref="PropertyChangedEventArgs"/> instance.</param>
|
||||
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the required configuration when a property is changing, and then
|
||||
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
|
||||
/// Raises the <see cref="PropertyChanging"/> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The input <see cref="PropertyChangingEventArgs"/> instance.</param>
|
||||
protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
|
||||
{
|
||||
PropertyChanging?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="PropertyChanged"/> event.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <remarks>The base implementation only raises the <see cref="PropertyChanging"/> event.</remarks>
|
||||
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="PropertyChanging"/> event.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -19,33 +19,28 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel
|
||||
/// </summary>
|
||||
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
|
||||
/// </summary>
|
||||
private static readonly PropertyChangedEventArgs HasErrorsChangedEventArgs = new PropertyChangedEventArgs(nameof(HasErrors));
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Dictionary{TKey,TValue}"/> instance used to store previous validation results.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, List<ValidationResult>> errors = new Dictionary<string, List<ValidationResult>>();
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the total number of properties with errors (not total errors).
|
||||
/// This is used to allow <see cref="HasErrors"/> to operate in O(1) time, as it can just
|
||||
/// check whether this value is not 0 instead of having to traverse <see cref="errors"/>.
|
||||
/// </summary>
|
||||
private int totalErrors;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasErrors
|
||||
{
|
||||
get
|
||||
{
|
||||
// This uses the value enumerator for Dictionary<TKey, TValue>.ValueCollection, so it doesn't
|
||||
// allocate. Accessing this property is O(n), but we can stop as soon as we find at least one
|
||||
// error in the whole entity, and doing this saves 8 bytes in the object size (no fields needed).
|
||||
foreach (var value in this.errors.Values)
|
||||
{
|
||||
if (value.Count > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public bool HasErrors => this.totalErrors > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
@ -67,12 +62,14 @@ public bool HasErrors
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
bool propertyChanged = SetProperty(ref field, newValue, propertyName);
|
||||
|
||||
if (propertyChanged && validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(ref field, newValue, propertyName);
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -90,12 +87,14 @@ protected bool SetProperty<T>(ref T field, T newValue, bool validate, [CallerMem
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);
|
||||
|
||||
if (propertyChanged && validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(ref field, newValue, comparer, propertyName);
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -120,12 +119,14 @@ protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comp
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName);
|
||||
|
||||
if (propertyChanged && validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, callback, propertyName);
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -144,12 +145,14 @@ protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool v
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName);
|
||||
|
||||
if (propertyChanged && validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, comparer, callback, propertyName);
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -172,12 +175,14 @@ protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> compa
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
if (validate)
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName);
|
||||
|
||||
if (propertyChanged && validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, model, callback, propertyName);
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -202,12 +207,123 @@ protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Acti
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
if (validate)
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
|
||||
|
||||
if (propertyChanged && validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to validate a new value for a specified property. If the validation is successful,
|
||||
/// <see cref="ObservableObject.SetProperty{T}(ref T,T,string?)"/> is called, otherwise no state change is performed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="errors">The resulting validation errors, if any.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
|
||||
protected bool TrySetProperty<T>(ref T field, T newValue, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return TryValidateProperty(newValue, propertyName, out errors) &&
|
||||
SetProperty(ref field, newValue, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to validate a new value for a specified property. If the validation is successful,
|
||||
/// <see cref="ObservableObject.SetProperty{T}(ref T,T,IEqualityComparer{T},string?)"/> is called, otherwise no state change is performed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="errors">The resulting validation errors, if any.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
|
||||
protected bool TrySetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return TryValidateProperty(newValue, propertyName, out errors) &&
|
||||
SetProperty(ref field, newValue, comparer, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to validate a new value for a specified property. If the validation is successful,
|
||||
/// <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string?)"/> is called, otherwise no state change is performed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="errors">The resulting validation errors, if any.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
|
||||
protected bool TrySetProperty<T>(T oldValue, T newValue, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return TryValidateProperty(newValue, propertyName, out errors) &&
|
||||
SetProperty(oldValue, newValue, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to validate a new value for a specified property. If the validation is successful,
|
||||
/// <see cref="ObservableObject.SetProperty{T}(T,T,IEqualityComparer{T},Action{T},string?)"/> is called, otherwise no state change is performed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="errors">The resulting validation errors, if any.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
|
||||
protected bool TrySetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return TryValidateProperty(newValue, propertyName, out errors) &&
|
||||
SetProperty(oldValue, newValue, comparer, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to validate a new value for a specified property. If the validation is successful,
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="model">The model </param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="errors">The resulting validation errors, if any.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
|
||||
protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
return TryValidateProperty(newValue, propertyName, out errors) &&
|
||||
SetProperty(oldValue, newValue, model, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to validate a new value for a specified property. If the validation is successful,
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="model">The model </param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="errors">The resulting validation errors, if any.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
|
||||
protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
return TryValidateProperty(newValue, propertyName, out errors) &&
|
||||
SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -285,6 +401,34 @@ private void ValidateProperty(object? value, string? propertyName)
|
||||
new ValidationContext(this, null, null) { MemberName = propertyName },
|
||||
propertyErrors);
|
||||
|
||||
// Update the shared counter for the number of errors, and raise the
|
||||
// property changed event if necessary. We decrement the number of total
|
||||
// errors if the current property is valid but it wasn't so before this
|
||||
// validation, and we increment it if the validation failed after being
|
||||
// correct before. The property changed event is raised whenever the
|
||||
// number of total errors is either decremented to 0, or incremented to 1.
|
||||
if (isValid)
|
||||
{
|
||||
if (errorsChanged)
|
||||
{
|
||||
this.totalErrors--;
|
||||
|
||||
if (this.totalErrors == 0)
|
||||
{
|
||||
OnPropertyChanged(HasErrorsChangedEventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!errorsChanged)
|
||||
{
|
||||
this.totalErrors++;
|
||||
|
||||
if (this.totalErrors == 1)
|
||||
{
|
||||
OnPropertyChanged(HasErrorsChangedEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
// Only raise the event once if needed. This happens either when the target property
|
||||
// had existing errors and is now valid, or if the validation has failed and there are
|
||||
// new errors to broadcast, regardless of the previous validation state for the property.
|
||||
@ -294,6 +438,61 @@ private void ValidateProperty(object? value, string? propertyName)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to validate a property with a specified name and a given input value, and returns
|
||||
/// the computed errors, if any. If the property is valid, it is assumed that its value is
|
||||
/// about to be set in the current object. Otherwise, no observable local state is modified.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to test for the specified property.</param>
|
||||
/// <param name="propertyName">The name of the property to validate.</param>
|
||||
/// <param name="errors">The resulting validation errors, if any.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is <see langword="null"/>.</exception>
|
||||
private bool TryValidateProperty(object? value, string? propertyName, out IReadOnlyCollection<ValidationResult> errors)
|
||||
{
|
||||
if (propertyName is null)
|
||||
{
|
||||
ThrowArgumentNullExceptionForNullPropertyName();
|
||||
}
|
||||
|
||||
// Add the cached errors list for later use.
|
||||
if (!this.errors.TryGetValue(propertyName!, out List<ValidationResult>? propertyErrors))
|
||||
{
|
||||
propertyErrors = new List<ValidationResult>();
|
||||
|
||||
this.errors.Add(propertyName!, propertyErrors);
|
||||
}
|
||||
|
||||
bool hasErrors = propertyErrors.Count > 0;
|
||||
|
||||
List<ValidationResult> localErrors = new List<ValidationResult>();
|
||||
|
||||
// Validate the property, by adding new errors to the local list
|
||||
bool isValid = Validator.TryValidateProperty(
|
||||
value,
|
||||
new ValidationContext(this, null, null) { MemberName = propertyName },
|
||||
localErrors);
|
||||
|
||||
// We only modify the state if the property is valid and it wasn't so before. In this case, we
|
||||
// clear the cached list of errors (which is visible to consumers) and raise the necessary events.
|
||||
if (isValid && hasErrors)
|
||||
{
|
||||
propertyErrors.Clear();
|
||||
|
||||
this.totalErrors--;
|
||||
|
||||
if (this.totalErrors == 0)
|
||||
{
|
||||
OnPropertyChanged(HasErrorsChangedEventArgs);
|
||||
}
|
||||
|
||||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
errors = localErrors;
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
#pragma warning disable SA1204
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentNullException"/> when a property name given as input is <see langword="null"/>.
|
||||
|
167
Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs
Normal file
167
Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs
Normal file
@ -0,0 +1,167 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// A type that facilitates the use of the <see cref="IServiceProvider"/> type.
|
||||
/// The <see cref="Ioc"/> provides the ability to configure services in a singleton, thread-safe
|
||||
/// service provider instance, which can then be used to resolve service instances.
|
||||
/// The first step to use this feature is to declare some services, for instance:
|
||||
/// <code>
|
||||
/// public interface ILogger
|
||||
/// {
|
||||
/// void Log(string text);
|
||||
/// }
|
||||
/// </code>
|
||||
/// <code>
|
||||
/// public class ConsoleLogger : ILogger
|
||||
/// {
|
||||
/// void Log(string text) => Console.WriteLine(text);
|
||||
/// }
|
||||
/// </code>
|
||||
/// Then the services configuration should then be done at startup, by calling the <see cref="ConfigureServices"/>
|
||||
/// method and passing an <see cref="IServiceProvider"/> instance with the services to use. That instance can
|
||||
/// be from any library offering dependency injection functionality, such as Microsoft.Extensions.DependencyInjection.
|
||||
/// For instance, using that library, <see cref="ConfigureServices"/> can be used as follows in this example:
|
||||
/// <code>
|
||||
/// Ioc.Default.ConfigureServices(
|
||||
/// new ServiceCollection()
|
||||
/// .AddSingleton<ILogger, Logger>()
|
||||
/// .BuildServiceProvider());
|
||||
/// </code>
|
||||
/// Finally, you can use the <see cref="Ioc"/> instance (which implements <see cref="IServiceProvider"/>)
|
||||
/// to retrieve the service instances from anywhere in your application, by doing as follows:
|
||||
/// <code>
|
||||
/// Ioc.Default.GetService<ILogger>().Log("Hello world!");
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public sealed class Ioc : IServiceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="Ioc"/> instance.
|
||||
/// </summary>
|
||||
public static Ioc Default { get; } = new Ioc();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IServiceProvider"/> instance to use, if initialized.
|
||||
/// </summary>
|
||||
private volatile IServiceProvider? serviceProvider;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object? GetService(Type serviceType)
|
||||
{
|
||||
// As per section I.12.6.6 of the official CLI ECMA-335 spec:
|
||||
// "[...] read and write access to properly aligned memory locations no larger than the native
|
||||
// word size is atomic when all the write accesses to a location are the same size. Atomic writes
|
||||
// shall alter no bits other than those written. Unless explicit layout control is used [...],
|
||||
// data elements no larger than the natural word size [...] shall be properly aligned.
|
||||
// Object references shall be treated as though they are stored in the native word size."
|
||||
// The field being accessed here is of native int size (reference type), and is only ever accessed
|
||||
// directly and atomically by a compare exchange instruction (see below), or here. We can therefore
|
||||
// assume this read is thread safe with respect to accesses to this property or to invocations to one
|
||||
// of the available configuration methods. So we can just read the field directly and make the necessary
|
||||
// check with our local copy, without the need of paying the locking overhead from this get accessor.
|
||||
IServiceProvider? provider = this.serviceProvider;
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForMissingInitialization();
|
||||
}
|
||||
|
||||
return provider!.GetService(serviceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve an instance of a specified service type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to resolve.</typeparam>
|
||||
/// <returns>An instance of the specified service, or <see langword="null"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Throw if the current <see cref="Ioc"/> instance has not been initialized.</exception>
|
||||
public T? GetService<T>()
|
||||
where T : class
|
||||
{
|
||||
IServiceProvider? provider = this.serviceProvider;
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForMissingInitialization();
|
||||
}
|
||||
|
||||
return (T?)provider!.GetService(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an instance of a specified service type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to resolve.</typeparam>
|
||||
/// <returns>An instance of the specified service, or <see langword="null"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Throw if the current <see cref="Ioc"/> instance has not been initialized, or if the
|
||||
/// requested service type was not registered in the service provider currently in use.
|
||||
/// </exception>
|
||||
public T GetRequiredService<T>()
|
||||
where T : class
|
||||
{
|
||||
IServiceProvider? provider = this.serviceProvider;
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForMissingInitialization();
|
||||
}
|
||||
|
||||
T? service = (T?)provider!.GetService(typeof(T));
|
||||
|
||||
if (service is null)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForUnregisteredType();
|
||||
}
|
||||
|
||||
return service!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the shared <see cref="IServiceProvider"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The input <see cref="IServiceProvider"/> instance to use.</param>
|
||||
public void ConfigureServices(IServiceProvider serviceProvider)
|
||||
{
|
||||
IServiceProvider? oldServices = Interlocked.CompareExchange(ref this.serviceProvider, serviceProvider, null);
|
||||
|
||||
if (!(oldServices is null))
|
||||
{
|
||||
ThrowInvalidOperationExceptionForRepeatedConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when the <see cref="IServiceProvider"/> property is used before initialization.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForMissingInitialization()
|
||||
{
|
||||
throw new InvalidOperationException("The service provider has not been configured yet");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when the <see cref="IServiceProvider"/> property is missing a type registration.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForUnregisteredType()
|
||||
{
|
||||
throw new InvalidOperationException("The requested service type was not registered");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when a configuration is attempted more than once.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForRepeatedConfiguration()
|
||||
{
|
||||
throw new InvalidOperationException("The default service provider has already been configured");
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -18,6 +19,21 @@ namespace Microsoft.Toolkit.Mvvm.Input
|
||||
/// </summary>
|
||||
public sealed class AsyncRelayCommand : ObservableObject, IAsyncRelayCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="CanBeCanceled"/>.
|
||||
/// </summary>
|
||||
internal static readonly PropertyChangedEventArgs CanBeCanceledChangedEventArgs = new PropertyChangedEventArgs(nameof(CanBeCanceled));
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsCancellationRequested"/>.
|
||||
/// </summary>
|
||||
internal static readonly PropertyChangedEventArgs IsCancellationRequestedChangedEventArgs = new PropertyChangedEventArgs(nameof(IsCancellationRequested));
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsRunning"/>.
|
||||
/// </summary>
|
||||
internal static readonly PropertyChangedEventArgs IsRunningChangedEventArgs = new PropertyChangedEventArgs(nameof(IsRunning));
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute"/> is used.
|
||||
/// </summary>
|
||||
@ -91,15 +107,22 @@ public Task? ExecutionTask
|
||||
get => this.executionTask;
|
||||
private set
|
||||
{
|
||||
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
|
||||
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ =>
|
||||
{
|
||||
OnPropertyChanged(nameof(IsRunning));
|
||||
// When the task completes
|
||||
OnPropertyChanged(IsRunningChangedEventArgs);
|
||||
OnPropertyChanged(CanBeCanceledChangedEventArgs);
|
||||
}))
|
||||
{
|
||||
// When setting the task
|
||||
OnPropertyChanged(IsRunningChangedEventArgs);
|
||||
OnPropertyChanged(CanBeCanceledChangedEventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanBeCanceled => !(this.cancelableExecute is null);
|
||||
public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
|
||||
@ -142,7 +165,7 @@ public Task ExecuteAsync(object? parameter)
|
||||
|
||||
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
OnPropertyChanged(IsCancellationRequestedChangedEventArgs);
|
||||
|
||||
// Invoke the cancelable command delegate with a new linked token
|
||||
return ExecutionTask = this.cancelableExecute!(cancellationTokenSource.Token);
|
||||
@ -156,7 +179,8 @@ public void Cancel()
|
||||
{
|
||||
this.cancellationTokenSource?.Cancel();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
OnPropertyChanged(IsCancellationRequestedChangedEventArgs);
|
||||
OnPropertyChanged(CanBeCanceledChangedEventArgs);
|
||||
}
|
||||
}
|
||||
}
|
@ -91,15 +91,22 @@ public Task? ExecutionTask
|
||||
get => this.executionTask;
|
||||
private set
|
||||
{
|
||||
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
|
||||
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ =>
|
||||
{
|
||||
OnPropertyChanged(nameof(IsRunning));
|
||||
// When the task completes
|
||||
OnPropertyChanged(AsyncRelayCommand.IsRunningChangedEventArgs);
|
||||
OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
|
||||
}))
|
||||
{
|
||||
// When setting the task
|
||||
OnPropertyChanged(AsyncRelayCommand.IsRunningChangedEventArgs);
|
||||
OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanBeCanceled => !(this.cancelableExecute is null);
|
||||
public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
|
||||
@ -163,7 +170,7 @@ public Task ExecuteAsync(T parameter)
|
||||
|
||||
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
OnPropertyChanged(AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
|
||||
|
||||
// Invoke the cancelable command delegate with a new linked token
|
||||
return ExecutionTask = this.cancelableExecute!(parameter, cancellationTokenSource.Token);
|
||||
@ -183,7 +190,8 @@ public void Cancel()
|
||||
{
|
||||
this.cancellationTokenSource?.Cancel();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
OnPropertyChanged(AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
|
||||
OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
|
||||
}
|
||||
}
|
||||
}
|
@ -27,27 +27,10 @@ public static class IMessengerExtensions
|
||||
/// </summary>
|
||||
private static class MethodInfos
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="MethodInfos"/> class.
|
||||
/// </summary>
|
||||
static MethodInfos()
|
||||
{
|
||||
RegisterIRecipient = (
|
||||
from methodInfo in typeof(IMessengerExtensions).GetMethods()
|
||||
where methodInfo.Name == nameof(Register) &&
|
||||
methodInfo.IsGenericMethod &&
|
||||
methodInfo.GetGenericArguments().Length == 2
|
||||
let parameters = methodInfo.GetParameters()
|
||||
where parameters.Length == 3 &&
|
||||
parameters[1].ParameterType.IsGenericType &&
|
||||
parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(IRecipient<>)
|
||||
select methodInfo).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="MethodInfo"/> instance associated with <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/>.
|
||||
/// </summary>
|
||||
public static readonly MethodInfo RegisterIRecipient;
|
||||
public static readonly MethodInfo RegisterIRecipient = new Action<IMessenger, IRecipient<object>, Unit>(Register).Method.GetGenericMethodDefinition();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -65,19 +65,19 @@ public override bool Equals(object? obj)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
// To combine the two hashes, we can simply use the fast djb2 hash algorithm.
|
||||
// This is not a problem in this case since we already know that the base
|
||||
// RuntimeHelpers.GetHashCode method is providing hashes with a good enough distribution.
|
||||
int hash = RuntimeHelpers.GetHashCode(TMessage);
|
||||
// To combine the two hashes, we can simply use the fast djb2 hash algorithm. Unfortunately we
|
||||
// can't really skip the callvirt here (eg. by using RuntimeHelpers.GetHashCode like in other
|
||||
// cases), as there are some niche cases mentioned above that might break when doing so.
|
||||
// However since this method is not generally used in a hot path (eg. the message broadcasting
|
||||
// only invokes this a handful of times when initially retrieving the target mapping), this
|
||||
// doesn't actually make a noticeable difference despite the minor overhead of the virtual call.
|
||||
int hash = TMessage.GetHashCode();
|
||||
|
||||
hash = (hash << 5) + hash;
|
||||
hash = (hash << 5) + hash;
|
||||
|
||||
hash += RuntimeHelpers.GetHashCode(TToken);
|
||||
hash += TToken.GetHashCode();
|
||||
|
||||
return hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -371,8 +371,6 @@ public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
|
||||
// that doesn't expose the single standard Current property.
|
||||
while (mappingEnumerator.MoveNext())
|
||||
{
|
||||
object recipient = mappingEnumerator.Key.Target;
|
||||
|
||||
// Pick the target handler, if the token is a match for the recipient
|
||||
if (mappingEnumerator.Value.TryGetValue(token, out object? handler))
|
||||
{
|
||||
@ -382,7 +380,7 @@ public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
|
||||
// We're still using a checked span accesses here though to make sure an out of
|
||||
// bounds write can never happen even if an error was present in the logic above.
|
||||
pairs[2 * i] = handler!;
|
||||
pairs[(2 * i) + 1] = recipient;
|
||||
pairs[(2 * i) + 1] = mappingEnumerator.Key.Target;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
<!-- .NET Core 2.1 doesn't have the Unsafe type -->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@ -17,6 +18,44 @@ namespace UnitTests.HighPerformance.Buffers
|
||||
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Test class for generic type")]
|
||||
public class Test_ArrayPoolBufferWriterOfT
|
||||
{
|
||||
[TestCategory("ArrayPoolBufferWriterOfT")]
|
||||
[TestMethod]
|
||||
[DataRow(0, 256)] // 256 is the default initial size for ArrayPoolBufferWriter<T>
|
||||
[DataRow(4, 256)]
|
||||
[DataRow(7, 256)]
|
||||
[DataRow(27, 256)]
|
||||
[DataRow(188, 256)]
|
||||
[DataRow(257, 512)]
|
||||
[DataRow(358, 512)]
|
||||
[DataRow(799, 1024)]
|
||||
[DataRow(1024, 1024)]
|
||||
[DataRow(1025, 2048)]
|
||||
[DataRow((1024 * 1024) - 1, 1024 * 1024)]
|
||||
[DataRow(1024 * 1024, 1024 * 1024)]
|
||||
[DataRow((1024 * 1024) + 1, 2 * 1024 * 1024)]
|
||||
[DataRow(2 * 1024 * 1024, 2 * 1024 * 1024)]
|
||||
[DataRow((2 * 1024 * 1024) + 1, 4 * 1024 * 1024)]
|
||||
[DataRow(3 * 1024 * 1024, 4 * 1024 * 1024)]
|
||||
public void Test_ArrayPoolBufferWriterOfT_BufferSize(int request, int expected)
|
||||
{
|
||||
using var writer = new ArrayPoolBufferWriter<byte>();
|
||||
|
||||
// Request a Span<T> of a specified size and discard it. We're just invoking this
|
||||
// method to force the ArrayPoolBufferWriter<T> instance to internally resize the
|
||||
// buffer to ensure it can contain at least this number of items. After this, we
|
||||
// can use reflection to get the internal array and ensure the size equals the
|
||||
// expected one, which matches the "round up to power of 2" logic we need. This
|
||||
// is documented within the resize method in ArrayPoolBufferWriter<T>, and it's
|
||||
// done to prevent repeated allocations of arrays in some scenarios.
|
||||
_ = writer.GetSpan(request);
|
||||
|
||||
var arrayFieldInfo = typeof(ArrayPoolBufferWriter<byte>).GetField("array", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
byte[] array = (byte[])arrayFieldInfo!.GetValue(writer);
|
||||
|
||||
Assert.AreEqual(array!.Length, expected);
|
||||
}
|
||||
|
||||
[TestCategory("ArrayPoolBufferWriterOfT")]
|
||||
[TestMethod]
|
||||
public void Test_ArrayPoolBufferWriterOfT_AllocateAndGetMemoryAndSpan()
|
||||
|
@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.HighPerformance.Buffers;
|
||||
@ -105,7 +104,7 @@ public void Test_MemoryOwnerOfT_MultipleDispose()
|
||||
// by accident doesn't cause issues, and just does nothing.
|
||||
}
|
||||
|
||||
[TestCategory("HashCodeOfT")]
|
||||
[TestCategory("MemoryOwnerOfT")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryOwnerOfT_PooledBuffersAndClear()
|
||||
{
|
||||
@ -124,5 +123,44 @@ public void Test_MemoryOwnerOfT_PooledBuffersAndClear()
|
||||
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 0));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCategory("MemoryOwnerOfT")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryOwnerOfT_AllocateAndGetArray()
|
||||
{
|
||||
var buffer = MemoryOwner<int>.Allocate(127);
|
||||
|
||||
// Here we allocate a MemoryOwner<T> instance with a requested size of 127, which means it
|
||||
// internally requests an array of size 127 from ArrayPool<T>.Shared. We then get the array
|
||||
// segment, so we need to verify that (since buffer is not disposed) the returned array is
|
||||
// not null, is of size >= the requested one (since ArrayPool<T> by definition returns an
|
||||
// array that is at least of the requested size), and that the offset and count properties
|
||||
// match our input values (same length, and offset at 0 since the buffer was not sliced).
|
||||
var segment = buffer.DangerousGetArray();
|
||||
|
||||
Assert.IsNotNull(segment.Array);
|
||||
Assert.IsTrue(segment.Array.Length >= buffer.Length);
|
||||
Assert.AreEqual(segment.Offset, 0);
|
||||
Assert.AreEqual(segment.Count, buffer.Length);
|
||||
|
||||
var second = buffer.Slice(10, 80);
|
||||
|
||||
// The original buffer instance is disposed here, because calling Slice transfers
|
||||
// the ownership of the internal buffer to the new instance (this is documented in
|
||||
// XML docs for the MemoryOwner<T>.Slice method).
|
||||
Assert.ThrowsException<ObjectDisposedException>(() => buffer.DangerousGetArray());
|
||||
|
||||
segment = second.DangerousGetArray();
|
||||
|
||||
// Same as before, but we now also verify the initial offset != 0, as we used Slice
|
||||
Assert.IsNotNull(segment.Array);
|
||||
Assert.IsTrue(segment.Array.Length >= second.Length);
|
||||
Assert.AreEqual(segment.Offset, 10);
|
||||
Assert.AreEqual(segment.Count, second.Length);
|
||||
|
||||
second.Dispose();
|
||||
|
||||
Assert.ThrowsException<ObjectDisposedException>(() => second.DangerousGetArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ public void Test_SpanOwnerOfT_InvalidRequestedSize()
|
||||
Assert.Fail("You shouldn't be here");
|
||||
}
|
||||
|
||||
[TestCategory("HashCodeOfT")]
|
||||
[TestCategory("SpanOwnerOfT")]
|
||||
[TestMethod]
|
||||
public void Test_SpanOwnerOfT_PooledBuffersAndClear()
|
||||
{
|
||||
@ -79,5 +79,23 @@ public void Test_SpanOwnerOfT_PooledBuffersAndClear()
|
||||
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 0));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCategory("SpanOwnerOfT")]
|
||||
[TestMethod]
|
||||
public void Test_SpanOwnerOfT_AllocateAndGetArray()
|
||||
{
|
||||
using var buffer = SpanOwner<int>.Allocate(127);
|
||||
|
||||
var segment = buffer.DangerousGetArray();
|
||||
|
||||
// See comments in the MemoryOwner<T> tests about this. The main difference
|
||||
// here is that we don't do the disposed checks, as SpanOwner<T> is optimized
|
||||
// with the assumption that usages after dispose are undefined behavior. This
|
||||
// is all documented in the XML docs for the SpanOwner<T> type.
|
||||
Assert.IsNotNull(segment.Array);
|
||||
Assert.IsTrue(segment.Array.Length >= buffer.Length);
|
||||
Assert.AreEqual(segment.Offset, 0);
|
||||
Assert.AreEqual(segment.Count, buffer.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,10 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
@ -12,12 +15,565 @@ namespace UnitTests.HighPerformance.Extensions
|
||||
[TestClass]
|
||||
public class Test_MemoryExtensions
|
||||
{
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_Cast_Empty()
|
||||
{
|
||||
// Casting an empty memory of any size should always be valid
|
||||
// and result in another empty memory, regardless of types.
|
||||
Memory<byte> m1 = default;
|
||||
Memory<byte> mc1 = m1.Cast<byte, byte>();
|
||||
|
||||
Assert.IsTrue(mc1.IsEmpty);
|
||||
|
||||
Memory<byte> m2 = default;
|
||||
Memory<float> mc2 = m2.Cast<byte, float>();
|
||||
|
||||
Assert.IsTrue(mc2.IsEmpty);
|
||||
|
||||
Memory<short> m3 = default;
|
||||
Memory<Guid> mc3 = m3.Cast<short, Guid>();
|
||||
|
||||
Assert.IsTrue(mc3.IsEmpty);
|
||||
|
||||
// Same as above, but with a sliced memory (length 12, slide from 12, so length of 0)
|
||||
Memory<byte> m4 = new byte[12].AsMemory(12);
|
||||
Memory<int> mc4 = m4.Cast<byte, int>();
|
||||
|
||||
Assert.IsTrue(mc4.IsEmpty);
|
||||
|
||||
// Same as above, but slicing to 12 in two steps
|
||||
Memory<byte> m5 = new byte[12].AsMemory().Slice(4).Slice(8);
|
||||
Memory<int> mc5 = m5.Cast<byte, int>();
|
||||
|
||||
Assert.IsTrue(mc5.IsEmpty);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_Cast_TooShort()
|
||||
{
|
||||
// One int is 4 bytes, so casting from 3 rounds down to 0
|
||||
Memory<byte> m1 = new byte[3];
|
||||
Memory<int> mc1 = m1.Cast<byte, int>();
|
||||
|
||||
Assert.IsTrue(mc1.IsEmpty);
|
||||
|
||||
// Same as above, 13 / sizeof(int) == 3
|
||||
Memory<byte> m2 = new byte[13];
|
||||
Memory<float> mc2 = m2.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(mc2.Length, 3);
|
||||
|
||||
// 16 - 5 = 11 ---> 11 / sizeof(int) = 2
|
||||
Memory<byte> m3 = new byte[16].AsMemory(5);
|
||||
Memory<float> mc3 = m3.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(mc3.Length, 2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastFromByte()
|
||||
{
|
||||
// Test for a standard cast from bytes with an evenly divisible length
|
||||
Memory<byte> memoryOfBytes = new byte[128];
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
|
||||
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
|
||||
// We also need to check that the Span<T> returned from the caast memory
|
||||
// actually has the initial reference pointing to the same location as
|
||||
// the one to the same item in the span from the original memory.
|
||||
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfBytes[0],
|
||||
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastToByte()
|
||||
{
|
||||
// Cast from float to bytes to verify casting works when the target type
|
||||
// as a smaller byte size as well (so the resulting length will be larger).
|
||||
Memory<float> memoryOfFloats = new float[128];
|
||||
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
|
||||
// Same as above, we need to verify that the resulting span has matching
|
||||
// starting references with the one produced by the original memory.
|
||||
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastToShort()
|
||||
{
|
||||
// Similar test as above, just with different types to double check
|
||||
Memory<float> memoryOfFloats = new float[128];
|
||||
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<short> spanOfShorts = memoryOfShorts.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastFromByteAndBack()
|
||||
{
|
||||
// Here we start from a byte array, get a memory, then cast to float and then
|
||||
// back to byte. We want to verify that the final memory is both valid and
|
||||
// consistent, as well that our internal optimization works and that the final
|
||||
// memory correctly skipped the indirect memory managed and just wrapped the original
|
||||
// array instead. This is documented in the custom array memory manager in the package.
|
||||
var data = new byte[128];
|
||||
Memory<byte> memoryOfBytes = data;
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
|
||||
|
||||
// Here we get the array from the final memory and check that it does exist and
|
||||
// the associated parameters match the ones we'd expect here (same length, offset of 0).
|
||||
Assert.IsTrue(MemoryMarshal.TryGetArray<byte>(memoryBack, out var segment));
|
||||
Assert.AreSame(segment.Array!, data);
|
||||
Assert.AreEqual(segment.Offset, 0);
|
||||
Assert.AreEqual(segment.Count, data.Length);
|
||||
|
||||
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
|
||||
|
||||
Span<byte> span1 = memoryOfBytes.Span;
|
||||
Span<byte> span2 = memoryBack.Span;
|
||||
|
||||
// Also validate the initial and final spans for reference equality
|
||||
Assert.IsTrue(span1 == span2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_Cast_TooShort_WithSlice()
|
||||
{
|
||||
// Like we did above, we have some more tests where we slice an initial memory and
|
||||
// validate the length of the resulting, accounting for the expected rounding down.
|
||||
Memory<byte> m1 = new byte[8].AsMemory().Slice(4, 3);
|
||||
Memory<int> mc1 = m1.Cast<byte, int>();
|
||||
|
||||
Assert.IsTrue(mc1.IsEmpty);
|
||||
|
||||
Memory<byte> m2 = new byte[20].AsMemory().Slice(4, 13);
|
||||
Memory<float> mc2 = m2.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(mc2.Length, 3);
|
||||
|
||||
Memory<byte> m3 = new byte[16].AsMemory().Slice(5);
|
||||
Memory<float> mc3 = m3.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(mc3.Length, 2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastFromByte_WithSlice()
|
||||
{
|
||||
// Same exact test as the cast from byte to float did above, but with a slice. This is done
|
||||
// to ensure the cast still works correctly when the memory is internally storing an offset.
|
||||
Memory<byte> memoryOfBytes = new byte[512].AsMemory().Slice(128, 128);
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
|
||||
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfBytes[0],
|
||||
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastToByte_WithSlice()
|
||||
{
|
||||
// Same as above, just with inverted source and destination types
|
||||
Memory<float> memoryOfFloats = new float[512].AsMemory().Slice(128, 128);
|
||||
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastToShort_WithSlice()
|
||||
{
|
||||
// Once again the same test but with types both different in size than 1. We're mostly
|
||||
// just testing the rounding logic in a number of different case to ensure it's correct.
|
||||
Memory<float> memoryOfFloats = new float[512].AsMemory().Slice(128, 128);
|
||||
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<short> spanOfShorts = memoryOfShorts.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromArray_CastFromByteAndBack_WithSlice()
|
||||
{
|
||||
// Just like the equivalent test above, but with a slice thrown in too
|
||||
var data = new byte[512];
|
||||
Memory<byte> memoryOfBytes = data.AsMemory().Slice(128, 128);
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
|
||||
|
||||
// Here we now also have to validate the starting offset from the extracted array
|
||||
Assert.IsTrue(MemoryMarshal.TryGetArray<byte>(memoryBack, out var segment));
|
||||
Assert.AreSame(segment.Array!, data);
|
||||
Assert.AreEqual(segment.Offset, 128);
|
||||
Assert.AreEqual(segment.Count, 128);
|
||||
|
||||
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
|
||||
|
||||
Span<byte> span1 = memoryOfBytes.Span;
|
||||
Span<byte> span2 = memoryBack.Span;
|
||||
|
||||
Assert.IsTrue(span1 == span2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastFromByte()
|
||||
{
|
||||
// This test is just like the ones above, but this time we're casting a memory
|
||||
// that wraps a custom memory manager and not an array. This is done to ensure
|
||||
// the casting logic works correctly in all cases, as it'll use a different
|
||||
// memory manager internally (a memory can wrap a string, an array or a manager).
|
||||
Memory<byte> memoryOfBytes = new ArrayMemoryManager<byte>(128);
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
|
||||
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfBytes[0],
|
||||
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastToByte()
|
||||
{
|
||||
// Same as above, but with inverted types
|
||||
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(128);
|
||||
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastToShort()
|
||||
{
|
||||
// Same as above, but with types different in size than 1, just in case
|
||||
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(128);
|
||||
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<short> spanOfShorts = memoryOfShorts.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack()
|
||||
{
|
||||
// Equivalent to the one with an array, but with a memory manager
|
||||
var data = new ArrayMemoryManager<byte>(128);
|
||||
Memory<byte> memoryOfBytes = data;
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
|
||||
|
||||
// Here we expect to get back the original memory manager, due to the same optimization we
|
||||
// checked for when using an array. We need to check they're the same, and the other parameters.
|
||||
Assert.IsTrue(MemoryMarshal.TryGetMemoryManager<byte, ArrayMemoryManager<byte>>(memoryBack, out var manager, out var start, out var length));
|
||||
Assert.AreSame(manager!, data);
|
||||
Assert.AreEqual(start, 0);
|
||||
Assert.AreEqual(length, 128);
|
||||
|
||||
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
|
||||
|
||||
Span<byte> span1 = memoryOfBytes.Span;
|
||||
Span<byte> span2 = memoryBack.Span;
|
||||
|
||||
Assert.IsTrue(span1 == span2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastFromByte_WithSlice()
|
||||
{
|
||||
// Same as the ones with an array, but with an extra slice
|
||||
Memory<byte> memoryOfBytes = new ArrayMemoryManager<byte>(512).Memory.Slice(128, 128);
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
|
||||
Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
|
||||
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfBytes[0],
|
||||
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastToByte_WithSlice()
|
||||
{
|
||||
// Same as above, but with inverted types
|
||||
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(512).Memory.Slice(128, 128);
|
||||
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<byte> spanOfBytes = memoryOfBytes.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastToShort_WithSlice()
|
||||
{
|
||||
// Same as above but with different types
|
||||
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(512).Memory.Slice(128, 128);
|
||||
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
|
||||
|
||||
Span<float> spanOfFloats = memoryOfFloats.Span;
|
||||
Span<short> spanOfShorts = memoryOfShorts.Span;
|
||||
|
||||
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
|
||||
Assert.IsTrue(Unsafe.AreSame(
|
||||
ref spanOfFloats[0],
|
||||
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack_WithSlice()
|
||||
{
|
||||
// Just like the one above, but with the slice
|
||||
var data = new ArrayMemoryManager<byte>(512);
|
||||
Memory<byte> memoryOfBytes = data.Memory.Slice(128, 128);
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
|
||||
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
|
||||
|
||||
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
|
||||
|
||||
// Here we also need to validate that the offset was maintained
|
||||
Assert.IsTrue(MemoryMarshal.TryGetMemoryManager<byte, ArrayMemoryManager<byte>>(memoryBack, out var manager, out var start, out var length));
|
||||
Assert.AreSame(manager!, data);
|
||||
Assert.AreEqual(start, 128);
|
||||
Assert.AreEqual(length, 128);
|
||||
|
||||
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
|
||||
|
||||
Span<byte> span1 = memoryOfBytes.Span;
|
||||
Span<byte> span2 = memoryBack.Span;
|
||||
|
||||
Assert.IsTrue(span1 == span2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
[DataRow(64, 0, 0)]
|
||||
[DataRow(64, 4, 0)]
|
||||
[DataRow(64, 0, 4)]
|
||||
[DataRow(64, 4, 4)]
|
||||
[DataRow(64, 4, 0)]
|
||||
[DataRow(256, 16, 0)]
|
||||
[DataRow(256, 4, 16)]
|
||||
[DataRow(256, 64, 0)]
|
||||
[DataRow(256, 64, 8)]
|
||||
public unsafe void Test_MemoryExtensions_FromArray_CastFromByte_Pin(int size, int preOffset, int postOffset)
|
||||
{
|
||||
// Here we need to validate that pinning works correctly in a number of cases. First we allocate
|
||||
// an array of the requested size, then get a memory after slicing to a target position, then cast
|
||||
// and then slice again. We do so to ensure that pinning correctly tracks the correct index with
|
||||
// respect to the original array through a number of internal offsets. As in, when pinning the
|
||||
// final memory, our internal custom memory manager should be able to pin the item in the original
|
||||
// array at offset preOffset + (postOffset * sizeof(float)), accounting for the cast as well.
|
||||
var data = new byte[size];
|
||||
Memory<byte> memoryOfBytes = data.AsMemory(preOffset);
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>().Slice(postOffset);
|
||||
|
||||
using var handle = memoryOfFloats.Pin();
|
||||
|
||||
void* p1 = handle.Pointer;
|
||||
void* p2 = Unsafe.AsPointer(ref data[preOffset + (postOffset * sizeof(float))]);
|
||||
|
||||
Assert.IsTrue(p1 == p2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
[DataRow(64, 0, 0)]
|
||||
[DataRow(64, 4, 0)]
|
||||
[DataRow(64, 0, 4)]
|
||||
[DataRow(64, 4, 4)]
|
||||
[DataRow(64, 4, 0)]
|
||||
[DataRow(256, 16, 0)]
|
||||
[DataRow(256, 4, 16)]
|
||||
[DataRow(256, 64, 0)]
|
||||
[DataRow(256, 64, 8)]
|
||||
public unsafe void Test_MemoryExtensions_FromMemoryManager_CastFromByte_Pin(int size, int preOffset, int postOffset)
|
||||
{
|
||||
// Just like the test above, but this type the initial memory wraps a memory manager
|
||||
var data = new ArrayMemoryManager<byte>(size);
|
||||
Memory<byte> memoryOfBytes = data.Memory.Slice(preOffset);
|
||||
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>().Slice(postOffset);
|
||||
|
||||
using var handle = memoryOfFloats.Pin();
|
||||
|
||||
void* p1 = handle.Pointer;
|
||||
void* p2 = Unsafe.AsPointer(ref data.GetSpan()[preOffset + (postOffset * sizeof(float))]);
|
||||
|
||||
Assert.IsTrue(p1 == p2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_FromString_CastFromByteAndBack()
|
||||
{
|
||||
// This is the same as the tests above, but here we're testing the
|
||||
// other remaining case, that is when a memory is wrapping a string.
|
||||
var data = new string('a', 128);
|
||||
Memory<char> memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory());
|
||||
Memory<float> memoryOfFloats = memoryOfChars.Cast<char, float>();
|
||||
Memory<char> memoryBack = memoryOfFloats.Cast<float, char>();
|
||||
|
||||
Assert.AreEqual(memoryOfChars.Length, memoryBack.Length);
|
||||
|
||||
// Get the original string back (to validate the optimization too) and check the params
|
||||
Assert.IsTrue(MemoryMarshal.TryGetString(memoryOfChars, out var text, out int start, out int length));
|
||||
Assert.AreSame(text!, data);
|
||||
Assert.AreEqual(start, 0);
|
||||
Assert.AreEqual(length, data.Length);
|
||||
|
||||
Assert.IsTrue(memoryOfChars.Equals(memoryBack));
|
||||
|
||||
Span<char> span1 = memoryOfChars.Span;
|
||||
Span<char> span2 = memoryBack.Span;
|
||||
|
||||
Assert.IsTrue(span1 == span2);
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
[DataRow(64, 0, 0)]
|
||||
[DataRow(64, 4, 0)]
|
||||
[DataRow(64, 0, 4)]
|
||||
[DataRow(64, 4, 4)]
|
||||
[DataRow(64, 4, 0)]
|
||||
[DataRow(256, 16, 0)]
|
||||
[DataRow(256, 4, 16)]
|
||||
[DataRow(256, 64, 0)]
|
||||
[DataRow(256, 64, 8)]
|
||||
public unsafe void Test_MemoryExtensions_FromString_CastAndPin(int size, int preOffset, int postOffset)
|
||||
{
|
||||
// Same test as before to validate pinning, but starting from a string
|
||||
var data = new string('a', size);
|
||||
Memory<char> memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory()).Slice(preOffset);
|
||||
Memory<byte> memoryOfBytes = memoryOfChars.Cast<char, byte>().Slice(postOffset);
|
||||
|
||||
using (var handle1 = memoryOfBytes.Pin())
|
||||
{
|
||||
void* p1 = handle1.Pointer;
|
||||
void* p2 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(byte) / sizeof(char))));
|
||||
|
||||
Assert.IsTrue(p1 == p2);
|
||||
}
|
||||
|
||||
// Here we also add an extra test just like the one above, but casting to a type
|
||||
// that is bigger in byte size than char. Just to double check the casting logic.
|
||||
Memory<int> memoryOfInts = memoryOfChars.Cast<char, int>().Slice(postOffset);
|
||||
|
||||
using (var handle2 = memoryOfInts.Pin())
|
||||
{
|
||||
void* p3 = handle2.Pointer;
|
||||
void* p4 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(int) / sizeof(char))));
|
||||
|
||||
Assert.IsTrue(p3 == p4);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCategory("MemoryExtensions")]
|
||||
[TestMethod]
|
||||
public void Test_MemoryExtensions_EmptyMemoryStream()
|
||||
{
|
||||
Memory<byte> memory = default;
|
||||
|
||||
// Creating a stream from a default memory is valid, it's just empty
|
||||
Stream stream = memory.AsStream();
|
||||
|
||||
Assert.IsNotNull(stream);
|
||||
@ -37,5 +593,43 @@ public void Test_MemoryExtensions_MemoryStream()
|
||||
Assert.AreEqual(stream.Length, memory.Length);
|
||||
Assert.IsTrue(stream.CanWrite);
|
||||
}
|
||||
|
||||
private sealed class ArrayMemoryManager<T> : MemoryManager<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly T[] array;
|
||||
|
||||
public ArrayMemoryManager(int size)
|
||||
{
|
||||
this.array = new T[size];
|
||||
}
|
||||
|
||||
public override Span<T> GetSpan()
|
||||
{
|
||||
return this.array;
|
||||
}
|
||||
|
||||
public override unsafe MemoryHandle Pin(int elementIndex = 0)
|
||||
{
|
||||
GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned);
|
||||
ref T r0 = ref this.array[elementIndex];
|
||||
void* p = Unsafe.AsPointer(ref r0);
|
||||
|
||||
return new MemoryHandle(p, handle);
|
||||
}
|
||||
|
||||
public override void Unpin()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
|
||||
public static implicit operator Memory<T>(ArrayMemoryManager<T> memoryManager)
|
||||
{
|
||||
return memoryManager.Memory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
@ -3,6 +3,8 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Toolkit.Mvvm.Input;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@ -124,26 +126,57 @@ public async Task Test_AsyncRelayCommand_WithCancellation()
|
||||
{
|
||||
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
|
||||
|
||||
// We need to test the cancellation support here, so we use the overload with an input
|
||||
// parameter, which is a cancellation token. The token is the one that is internally managed
|
||||
// by the AsyncRelayCommand instance, and canceled when using IAsyncRelayCommand.Cancel().
|
||||
var command = new AsyncRelayCommand(token => tcs.Task);
|
||||
|
||||
List<PropertyChangedEventArgs> args = new List<PropertyChangedEventArgs>();
|
||||
|
||||
command.PropertyChanged += (s, e) => args.Add(e);
|
||||
|
||||
// We have no canExecute parameter, so the command can always be invoked
|
||||
Assert.IsTrue(command.CanExecute(null));
|
||||
Assert.IsTrue(command.CanExecute(new object()));
|
||||
|
||||
// The command isn't running, so it can't be canceled yet
|
||||
Assert.IsFalse(command.CanBeCanceled);
|
||||
Assert.IsFalse(command.IsCancellationRequested);
|
||||
|
||||
// Start the command, which will return the token from our task completion source.
|
||||
// We can use that to easily keep the command running while we do our tests, and then
|
||||
// stop the processing by completing the source when we need (see below).
|
||||
command.Execute(null);
|
||||
|
||||
// The command is running, so it can be canceled, as we used the token overload
|
||||
Assert.IsTrue(command.CanBeCanceled);
|
||||
Assert.IsFalse(command.IsCancellationRequested);
|
||||
|
||||
command.Execute(null);
|
||||
|
||||
Assert.IsFalse(command.IsCancellationRequested);
|
||||
// Validate the various event args for all the properties that were updated when executing the command
|
||||
Assert.AreEqual(args.Count, 4);
|
||||
Assert.AreEqual(args[0].PropertyName, nameof(IAsyncRelayCommand.IsCancellationRequested));
|
||||
Assert.AreEqual(args[1].PropertyName, nameof(IAsyncRelayCommand.ExecutionTask));
|
||||
Assert.AreEqual(args[2].PropertyName, nameof(IAsyncRelayCommand.IsRunning));
|
||||
Assert.AreEqual(args[3].PropertyName, nameof(IAsyncRelayCommand.CanBeCanceled));
|
||||
|
||||
command.Cancel();
|
||||
|
||||
// Verify that these two properties raised notifications correctly when canceling the command too.
|
||||
// We need to ensure all command properties support notifications so that users can bind to them.
|
||||
Assert.AreEqual(args.Count, 6);
|
||||
Assert.AreEqual(args[4].PropertyName, nameof(IAsyncRelayCommand.IsCancellationRequested));
|
||||
Assert.AreEqual(args[5].PropertyName, nameof(IAsyncRelayCommand.CanBeCanceled));
|
||||
|
||||
Assert.IsTrue(command.IsCancellationRequested);
|
||||
|
||||
// Complete the source, which will mark the command as completed too (as it returned the same task)
|
||||
tcs.SetResult(null);
|
||||
|
||||
await command.ExecutionTask!;
|
||||
|
||||
// Verify that the command can no longer be canceled, and that the cancellation is
|
||||
// instead still true, as that's reset when executing a command and not on completion.
|
||||
Assert.IsFalse(command.CanBeCanceled);
|
||||
Assert.IsTrue(command.IsCancellationRequested);
|
||||
}
|
||||
}
|
||||
|
@ -19,16 +19,28 @@ public class Test_ObservableValidator
|
||||
public void Test_ObservableValidator_HasErrors()
|
||||
{
|
||||
var model = new Person();
|
||||
var args = new List<PropertyChangedEventArgs>();
|
||||
|
||||
model.PropertyChanged += (s, e) => args.Add(e);
|
||||
|
||||
Assert.IsFalse(model.HasErrors);
|
||||
|
||||
model.Name = "No";
|
||||
|
||||
// Verify that errors were correctly reported as changed, and that all the relevant
|
||||
// properties were broadcast as well (both the changed property and HasErrors). We need
|
||||
// this last one to raise notifications too so that users can bind to that in the UI.
|
||||
Assert.IsTrue(model.HasErrors);
|
||||
Assert.AreEqual(args.Count, 2);
|
||||
Assert.AreEqual(args[0].PropertyName, nameof(Person.Name));
|
||||
Assert.AreEqual(args[1].PropertyName, nameof(INotifyDataErrorInfo.HasErrors));
|
||||
|
||||
model.Name = "Valid";
|
||||
|
||||
Assert.IsFalse(model.HasErrors);
|
||||
Assert.AreEqual(args.Count, 4);
|
||||
Assert.AreEqual(args[2].PropertyName, nameof(Person.Name));
|
||||
Assert.AreEqual(args[3].PropertyName, nameof(INotifyDataErrorInfo.HasErrors));
|
||||
}
|
||||
|
||||
[TestCategory("Mvvm")]
|
||||
@ -119,7 +131,6 @@ public void Test_ObservableValidator_GetErrors()
|
||||
|
||||
[TestCategory("Mvvm")]
|
||||
[TestMethod]
|
||||
[DataRow(null, false)]
|
||||
[DataRow("", false)]
|
||||
[DataRow("No", false)]
|
||||
[DataRow("This text is really, really too long for the target property", false)]
|
||||
@ -142,6 +153,60 @@ public void Test_ObservableValidator_ValidateReturn(string value, bool isValid)
|
||||
}
|
||||
}
|
||||
|
||||
[TestCategory("Mvvm")]
|
||||
[TestMethod]
|
||||
public void Test_ObservableValidator_TrySetProperty()
|
||||
{
|
||||
var model = new Person();
|
||||
var events = new List<DataErrorsChangedEventArgs>();
|
||||
|
||||
model.ErrorsChanged += (s, e) => events.Add(e);
|
||||
|
||||
// Set a correct value, this should update the property
|
||||
Assert.IsTrue(model.TrySetName("Hello", out var errors));
|
||||
Assert.IsTrue(errors.Count == 0);
|
||||
Assert.IsTrue(events.Count == 0);
|
||||
Assert.AreEqual(model.Name, "Hello");
|
||||
Assert.IsFalse(model.HasErrors);
|
||||
|
||||
// Invalid value #1, this should be ignored
|
||||
Assert.IsFalse(model.TrySetName(null, out errors));
|
||||
Assert.IsTrue(errors.Count > 0);
|
||||
Assert.IsTrue(events.Count == 0);
|
||||
Assert.AreEqual(model.Name, "Hello");
|
||||
Assert.IsFalse(model.HasErrors);
|
||||
|
||||
// Invalid value #2, same as above
|
||||
Assert.IsFalse(model.TrySetName("This string is too long for the target property in this model and should fail", out errors));
|
||||
Assert.IsTrue(errors.Count > 0);
|
||||
Assert.IsTrue(events.Count == 0);
|
||||
Assert.AreEqual(model.Name, "Hello");
|
||||
Assert.IsFalse(model.HasErrors);
|
||||
|
||||
// Correct value, this should update the property
|
||||
Assert.IsTrue(model.TrySetName("Hello world", out errors));
|
||||
Assert.IsTrue(errors.Count == 0);
|
||||
Assert.IsTrue(events.Count == 0);
|
||||
Assert.AreEqual(model.Name, "Hello world");
|
||||
Assert.IsFalse(model.HasErrors);
|
||||
|
||||
// Actually set an invalid value to show some errors
|
||||
model.Name = "No";
|
||||
|
||||
// Errors should now be present
|
||||
Assert.IsTrue(model.HasErrors);
|
||||
Assert.IsTrue(events.Count == 1);
|
||||
Assert.IsTrue(model.GetErrors(nameof(Person.Name)).Cast<ValidationResult>().Any());
|
||||
Assert.IsTrue(model.HasErrors);
|
||||
|
||||
// Trying to set a correct property should clear the errors
|
||||
Assert.IsTrue(model.TrySetName("This is fine", out errors));
|
||||
Assert.IsTrue(errors.Count == 0);
|
||||
Assert.IsTrue(events.Count == 2);
|
||||
Assert.IsFalse(model.HasErrors);
|
||||
Assert.AreEqual(model.Name, "This is fine");
|
||||
}
|
||||
|
||||
public class Person : ObservableValidator
|
||||
{
|
||||
private string name;
|
||||
@ -155,6 +220,11 @@ public string Name
|
||||
set => SetProperty(ref this.name, value, true);
|
||||
}
|
||||
|
||||
public bool TrySetName(string value, out IReadOnlyCollection<ValidationResult> errors)
|
||||
{
|
||||
return TrySetProperty(ref name, value, out errors, nameof(Name));
|
||||
}
|
||||
|
||||
private int age;
|
||||
|
||||
[Range(0, 100)]
|
||||
|
Loading…
Reference in New Issue
Block a user