Nullability of argument doesn't match constraint type

Viewed 1595

I've written the following extension method:

public static void NotifyChanged<T>(this INotifyPropertyChanged inpc, ref T current, T newValue, Action<PropertyChangedEventArgs> eventRaiser, [CallerMemberName] string? name = null) where T : IEquatable<T> {
    if (current.Equals(newValue)) { return; }
    current = newValue;
    eventRaiser(new PropertyChangedEventArgs(name));
}

that can be used like this:

public class Foo : Bar, INotifyPropertyChanged {
    public event PropertyChangedEventHandler? PropertyChanged;

    private string? rootExpression;

    public string? RootExpression {
        get => rootExpression;
        set => this.NotifyChanged(ref rootExpression, value, args => PropertyChanged?.Invoke(this, args));
    }
}

This saves much of the boilerplate of writing INPC-aware properties.

However, I now get a compiler warning error at the call to NotifyChanged:

The type 'string?' cannot be used as type parameter 'T' in the generic type or method 'INotifyPropertyChangedExtensions.NotifyChanged(INotifyPropertyChanged, ref T, T, Action, string?)'. Nullability of type argument 'string?' doesn't match constraint type 'System.IEquatable'.

AFAICT the error is saying that string? cannot be cast to IEquatable<string?>, only string can be cast to IEquatable<string>.

How can I resolve this? Apply some attribute? Or something else?

1 Answers

Your problem is:

where T : IEquatable<T>

This says that T must be a non-nullable IEquatable<T>. You want it to be nullable. You can say this by adding a ?:

where T : IEquatable<T>?

Note that this will then complain on if (current.Equals(newValue)), which will throw if current is null.


The normal way to do this is not by constraining T to be an IEquatable<T>, but to use EqualityComparer<T>.Default. If T implements IEquatable<T>, this gives you an equality comparer which calls IEquatable<T>.Equals, otherwise it falls back to calling the normal object.Equals.

This also solves your issue of an NRE if current is null:

public static void NotifyChanged<T>(
    this INotifyPropertyChanged inpc,
    ref T current, T newValue,
    Action<PropertyChangedEventArgs> eventRaiser,
    [CallerMemberName] string? name = null)
{
    if (EqualityComparer<T>.Default.Equals(current, newValue)) { return; }
    current = newValue;
    eventRaiser(new PropertyChangedEventArgs(name));
}

It's also rare to pass in eventRaiser: normally you'd make NotifyChanged a method on a base class which implements INotifyPropertyChanged, rather than making it an extension method. Then you can just let NotifyChanged raise the PropertyChanged event itself, or you write another method such as OnPropertyChanged which raises PropertyChanged and call that from NotifyChanged.

If you do want to pass in the event to raise, you could just pass in the PropertyChangedEventHandler:

public static void NotifyChanged<T>(
    this INotifyPropertyChanged _,
    ref T current, T newValue,
    PropertyChangedEventHandler eventHandler,
    [CallerMemberName] string? name = null)
{
    if (EqualityComparer<T>.Default.Equals(current, newValue)) { return; }
    current = newValue;
    eventHandler?.Invoke(this, new PropertyChangedEventArgs(name));
}

this.NotifyChanged(ref rootExpression, value, PropertyChanged);
Related