Use T? for unconstrained nullable types (#25261)

* Use T? for unconstrained nullable types

* Apply suggestions from code review
This commit is contained in:
Pranav K 2020-09-10 15:14:30 -07:00 committed by GitHub
parent 690c717314
commit 36f8642f0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 49 additions and 46 deletions

View File

@ -25,8 +25,8 @@ namespace Microsoft.AspNetCore.Components
private delegate object? BindFormatter<T>(T value, CultureInfo? culture); private delegate object? BindFormatter<T>(T value, CultureInfo? culture);
private delegate object BindFormatterWithFormat<T>(T value, CultureInfo? culture, string format); private delegate object BindFormatterWithFormat<T>(T value, CultureInfo? culture, string format);
internal delegate bool BindParser<T>(object? obj, CultureInfo? culture, out T value); internal delegate bool BindParser<T>(object? obj, CultureInfo? culture, [MaybeNullWhen(false)] out T value);
internal delegate bool BindParserWithFormat<T>(object? obj, CultureInfo? culture, string? format, out T value); internal delegate bool BindParserWithFormat<T>(object? obj, CultureInfo? culture, string? format, [MaybeNullWhen(false)] out T value);
/// <summary> /// <summary>
/// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>. /// Formats the provided <paramref name="value"/> as a <see cref="System.String"/>.
@ -1276,7 +1276,7 @@ namespace Microsoft.AspNetCore.Components
/// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param> /// <param name="culture">The <see cref="CultureInfo"/> to use for conversion.</param>
/// <param name="value">The converted value.</param> /// <param name="value">The converted value.</param>
/// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns> /// <returns><c>true</c> if conversion is successful, otherwise <c>false</c>.</returns>
public static bool TryConvertTo<T>(object? obj, CultureInfo? culture, out T value) public static bool TryConvertTo<T>(object? obj, CultureInfo? culture, [MaybeNullWhen(false)] out T value)
{ {
var converter = ParserDelegateCache.Get<T>(); var converter = ParserDelegateCache.Get<T>();
return converter(obj, culture, out value); return converter(obj, culture, out value);

View File

@ -46,11 +46,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary> /// </summary>
/// <param name="arg">The argument.</param> /// <param name="arg">The argument.</param>
/// <returns>A <see cref="Task"/> which completes asynchronously once event processing has completed.</returns> /// <returns>A <see cref="Task"/> which completes asynchronously once event processing has completed.</returns>
public Task InvokeAsync(TValue arg) public Task InvokeAsync(TValue? arg)
{ {
if (Receiver == null) if (Receiver == null)
{ {
return EventCallbackWorkItem.InvokeAsync<TValue>(Delegate, arg); return EventCallbackWorkItem.InvokeAsync<TValue?>(Delegate, arg);
} }
return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);

View File

@ -88,9 +88,8 @@ namespace Microsoft.AspNetCore.Components
/// <typeparam name="TValue">The type of the value.</typeparam> /// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="parameterName">The name of the parameter.</param> /// <param name="parameterName">The name of the parameter.</param>
/// <returns>The parameter value if found; otherwise the default value for the specified type.</returns> /// <returns>The parameter value if found; otherwise the default value for the specified type.</returns>
[return: MaybeNull] public TValue? GetValueOrDefault<TValue>(string parameterName)
public TValue GetValueOrDefault<TValue>(string parameterName) => GetValueOrDefault<TValue?>(parameterName, default);
=> GetValueOrDefault<TValue>(parameterName, default!);
/// <summary> /// <summary>
/// Gets the value of the parameter with the specified name, or a specified default value /// Gets the value of the parameter with the specified name, or a specified default value

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public string Secret { get; } public string Secret { get; }
public bool Equals([AllowNull] CircuitId other) public bool Equals(CircuitId other)
{ {
// We want to use a fixed time equality comparison for a *real* comparisons. // We want to use a fixed time equality comparison for a *real* comparisons.
// The only use case for Secret being null is with a default struct value, // The only use case for Secret being null is with a default struct value,

View File

@ -35,10 +35,8 @@ namespace Microsoft.AspNetCore.Components.Forms
/// <example> /// <example>
/// @bind-Value="model.PropertyName" /// @bind-Value="model.PropertyName"
/// </example> /// </example>
[AllowNull]
[MaybeNull]
[Parameter] [Parameter]
public TValue Value { get; set; } = default; public TValue? Value { get; set; }
/// <summary> /// <summary>
/// Gets or sets a callback that updates the bound value. /// Gets or sets a callback that updates the bound value.
@ -69,17 +67,15 @@ namespace Microsoft.AspNetCore.Components.Forms
/// <summary> /// <summary>
/// Gets or sets the current value of the input. /// Gets or sets the current value of the input.
/// </summary> /// </summary>
[AllowNull] protected TValue? CurrentValue
protected TValue CurrentValue
{ {
[return: MaybeNull] get => Value;
get => Value!;
set set
{ {
var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value); var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value);
if (hasChanged) if (hasChanged)
{ {
Value = value!; Value = value;
_ = ValueChanged.InvokeAsync(Value); _ = ValueChanged.InvokeAsync(Value);
EditContext.NotifyFieldChanged(FieldIdentifier); EditContext.NotifyFieldChanged(FieldIdentifier);
} }
@ -148,7 +144,7 @@ namespace Microsoft.AspNetCore.Components.Forms
/// </summary> /// </summary>
/// <param name="value">The value to format.</param> /// <param name="value">The value to format.</param>
/// <returns>A string representation of the value.</returns> /// <returns>A string representation of the value.</returns>
protected virtual string? FormatValueAsString([AllowNull] TValue value) protected virtual string? FormatValueAsString(TValue? value)
=> value?.ToString(); => value?.ToString();
/// <summary> /// <summary>
@ -159,7 +155,7 @@ namespace Microsoft.AspNetCore.Components.Forms
/// <param name="result">An instance of <typeparamref name="TValue"/>.</param> /// <param name="result">An instance of <typeparamref name="TValue"/>.</param>
/// <param name="validationErrorMessage">If the value could not be parsed, provides a validation error message.</param> /// <param name="validationErrorMessage">If the value could not be parsed, provides a validation error message.</param>
/// <returns>True if the value could be parsed; otherwise false.</returns> /// <returns>True if the value could be parsed; otherwise false.</returns>
protected abstract bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage); protected abstract bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage);
/// <summary> /// <summary>
/// Gets a string that indicates the status of the field being edited. This will include /// Gets a string that indicates the status of the field being edited. This will include

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
/// <inheritdoc /> /// <inheritdoc />
protected override string FormatValueAsString([AllowNull] TValue value) protected override string FormatValueAsString(TValue? value)
{ {
switch (value) switch (value)
{ {
@ -48,7 +49,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
/// <inheritdoc /> /// <inheritdoc />
protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{ {
// Unwrap nullable types. We don't have to deal with receiving empty values for nullable // Unwrap nullable types. We don't have to deal with receiving empty values for nullable
// types here, because the underlying InputBase already covers that. // types here, because the underlying InputBase already covers that.
@ -70,6 +71,7 @@ namespace Microsoft.AspNetCore.Components.Forms
if (success) if (success)
{ {
Debug.Assert(result != null);
validationErrorMessage = null; validationErrorMessage = null;
return true; return true;
} }
@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
} }
static bool TryParseDateTime(string? value, [MaybeNullWhen(false)] out TValue result) private static bool TryParseDateTime(string? value, [MaybeNullWhen(false)] out TValue result)
{ {
var success = BindConverter.TryConvertToDateTime(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue); var success = BindConverter.TryConvertToDateTime(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue);
if (success) if (success)
@ -95,7 +97,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
} }
static bool TryParseDateTimeOffset(string? value, [MaybeNullWhen(false)] out TValue result) private static bool TryParseDateTimeOffset(string? value, [MaybeNullWhen(false)] out TValue result)
{ {
var success = BindConverter.TryConvertToDateTimeOffset(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue); var success = BindConverter.TryConvertToDateTimeOffset(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue);
if (success) if (success)

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
internal static class InputExtensions internal static class InputExtensions
{ {
public static bool TryParseSelectableValueFromString<TValue>(this InputBase<TValue> input, string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) public static bool TryParseSelectableValueFromString<TValue>(this InputBase<TValue> input, string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{ {
try try
{ {

View File

@ -73,6 +73,11 @@ namespace Microsoft.AspNetCore.Components.Forms
{ {
var imageFile = await JSRuntime.InvokeAsync<BrowserFile>(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight); var imageFile = await JSRuntime.InvokeAsync<BrowserFile>(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight);
if (imageFile is null)
{
throw new InvalidOperationException("ToImageFile returned an unexpected null result.");
}
imageFile.Owner = this; imageFile.Owner = this;
return imageFile; return imageFile;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -64,10 +65,10 @@ namespace Microsoft.AspNetCore.Components.Forms
offset, offset,
segmentSize); segmentSize);
if (bytes.Length != segmentSize) if (bytes is null || bytes.Length != segmentSize)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"A segment with size {bytes.Length} bytes was received, but {segmentSize} bytes were expected."); $"A segment with size {bytes?.Length ?? 0} bytes was received, but {segmentSize} bytes were expected.");
} }
bytes.CopyTo(pipeBuffer); bytes.CopyTo(pipeBuffer);

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
/// <inheritdoc /> /// <inheritdoc />
protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{ {
if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.InvariantCulture, out result)) if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.InvariantCulture, out result))
{ {
@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Components.Forms
/// </summary> /// </summary>
/// <param name="value">The value to format.</param> /// <param name="value">The value to format.</param>
/// <returns>A string representation of the value.</returns> /// <returns>A string representation of the value.</returns>
protected override string? FormatValueAsString([AllowNull] TValue value) protected override string? FormatValueAsString(TValue? value)
{ {
// Avoiding a cast to IFormattable to avoid boxing. // Avoiding a cast to IFormattable to avoid boxing.
switch (value) switch (value)

View File

@ -27,10 +27,8 @@ namespace Microsoft.AspNetCore.Components.Forms
/// <summary> /// <summary>
/// Gets or sets the value of this input. /// Gets or sets the value of this input.
/// </summary> /// </summary>
[AllowNull]
[MaybeNull]
[Parameter] [Parameter]
public TValue Value { get; set; } = default; public TValue? Value { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the parent input radio group. /// Gets or sets the name of the parent input radio group.

View File

@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
/// <inheritdoc /> /// <inheritdoc />
protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
=> this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage); => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
} }
} }

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Components.Forms
} }
/// <inheritdoc /> /// <inheritdoc />
protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
=> this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage); => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
} }
} }

View File

@ -11,10 +11,10 @@ namespace Microsoft.JSInterop
/// <summary> /// <summary>
/// Invokes the specified JavaScript function synchronously. /// Invokes the specified JavaScript function synchronously.
/// </summary> /// </summary>
/// <typeparam name="T">The JSON-serializable return type.</typeparam> /// <typeparam name="TResult">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param> /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TResult"/> obtained by JSON-deserializing the return value.</returns>
T Invoke<T>(string identifier, params object?[]? args); TResult Invoke<TResult>(string identifier, params object?[]? args);
} }
} }

View File

@ -23,7 +23,6 @@ namespace Microsoft.JSInterop.Implementation
} }
/// <inheritdoc /> /// <inheritdoc />
[return: MaybeNull]
public TValue Invoke<TValue>(string identifier, params object?[]? args) public TValue Invoke<TValue>(string identifier, params object?[]? args)
{ {
ThrowIfDisposed(); ThrowIfDisposed();

View File

@ -23,7 +23,6 @@ namespace Microsoft.JSInterop
id => new JSInProcessObjectReference(this, id))); id => new JSInProcessObjectReference(this, id)));
} }
[return: MaybeNull]
internal TValue Invoke<TValue>(string identifier, long targetInstanceId, params object?[]? args) internal TValue Invoke<TValue>(string identifier, long targetInstanceId, params object?[]? args)
{ {
var resultJson = InvokeJS( var resultJson = InvokeJS(
@ -32,12 +31,15 @@ namespace Microsoft.JSInterop
JSCallResultTypeHelper.FromGeneric<TValue>(), JSCallResultTypeHelper.FromGeneric<TValue>(),
targetInstanceId); targetInstanceId);
// While the result of deserialization could be null, we're making a
// quality of life decision and letting users explicitly determine if they expect
// null by specifying TValue? as the expected return type.
if (resultJson is null) if (resultJson is null)
{ {
return default; return default!;
} }
return JsonSerializer.Deserialize<TValue>(resultJson, JsonSerializerOptions); return JsonSerializer.Deserialize<TValue>(resultJson, JsonSerializerOptions)!;
} }
/// <summary> /// <summary>
@ -47,7 +49,6 @@ namespace Microsoft.JSInterop
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param> /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
[return: MaybeNull]
public TValue Invoke<TValue>(string identifier, params object?[]? args) public TValue Invoke<TValue>(string identifier, params object?[]? args)
=> Invoke<TValue>(identifier, 0, args); => Invoke<TValue>(identifier, 0, args);

View File

@ -102,7 +102,7 @@ namespace Microsoft.JSInterop
{ {
var taskId = Interlocked.Increment(ref _nextPendingTaskId); var taskId = Interlocked.Increment(ref _nextPendingTaskId);
var tcs = new TaskCompletionSource<TValue>(); var tcs = new TaskCompletionSource<TValue>();
if (cancellationToken != default) if (cancellationToken.CanBeCanceled)
{ {
_cancellationRegistrations[taskId] = cancellationToken.Register(() => _cancellationRegistrations[taskId] = cancellationToken.Register(() =>
{ {

View File

@ -41,7 +41,7 @@ namespace Microsoft.JSInterop
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param> /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, params object[] args) public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, params object?[]? args)
{ {
if (jsRuntime is null) if (jsRuntime is null)
{ {
@ -63,7 +63,7 @@ namespace Microsoft.JSInterop
/// </param> /// </param>
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args) public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object?[]? args)
{ {
if (jsRuntime is null) if (jsRuntime is null)
{ {
@ -102,7 +102,7 @@ namespace Microsoft.JSInterop
/// <param name="timeout">The duration after which to cancel the async operation. Overrides default timeouts (<see cref="JSRuntime.DefaultAsyncTimeout"/>).</param> /// <param name="timeout">The duration after which to cancel the async operation. Overrides default timeouts (<see cref="JSRuntime.DefaultAsyncTimeout"/>).</param>
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>A <see cref="ValueTask"/> that represents the asynchronous invocation operation.</returns> /// <returns>A <see cref="ValueTask"/> that represents the asynchronous invocation operation.</returns>
public static async ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args) public static async ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object?[]? args)
{ {
if (jsRuntime is null) if (jsRuntime is null)
{ {

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -145,6 +146,7 @@ namespace Microsoft.JSInterop
ref reader); ref reader);
Assert.True(task.IsCompleted); Assert.True(task.IsCompleted);
var poco = task.Result; var poco = task.Result;
Debug.Assert(poco != null);
Assert.Equal(10, poco.Id); Assert.Equal(10, poco.Id);
Assert.Equal("Test", poco.Name); Assert.Equal("Test", poco.Name);
} }
@ -167,6 +169,7 @@ namespace Microsoft.JSInterop
ref reader); ref reader);
Assert.True(task.IsCompleted); Assert.True(task.IsCompleted);
var poco = task.Result; var poco = task.Result;
Debug.Assert(poco != null);
Assert.Equal(10, poco.Id); Assert.Equal(10, poco.Id);
Assert.Equal("Test", poco.Name); Assert.Equal("Test", poco.Name);
} }