csharplang/meetings/2019/LDM-2019-04-24.md
2019-04-26 16:39:12 -07:00

5.8 KiB
Raw Permalink Blame History

C# LDM Notes for April 24, 2019

Agenda

  1. MaybeNull and related attributes

Discussion

There are two proposals on the table:

The proposals are primarily focused on providing nullable information for the caller of methods where certain nullable contracts are outside the ability of our current annotation syntax to express.

We've also gathered the following information on nullable contracts as they currently exist in CoreFX:

Weve annotated most of corelib at this point. I just quickly went through looking at some of the ~600 TODO-NULLABLE follow-up comments we have for examples of public API signatures that are currently less-than-correct and that would benefit from further ability to annotate/attribute. I did not include things that just impact locals or fields or other non-API stuff.

Generic methods or method on generic type that might return default(T), e.g.

  • Array.Find(T[] array, Predicate match). Returns default(T) if there isnt a match.
  • Lazy.ValueForDebugDisplay. Returns default(T) if the Lazy hasnt yet been initialized.
  • Marshal.PtrToStructure(IntPtr). If you pass in IntPtr.Zero, youll get back default(T).

Generic methods accepting an unconstrained T with argument that should not be null

  • Marshal.GetFunctionPointerForDelegate

Try- methods on generic type that outs default(T), e.g.

  • ConcurrentQueue.TryDequeue(out T)
  • IProducerConsumerCollection.TryTake(out T)
  • WeakReference.TryGetTarget(out T target) Try- methods with non-generics, e.g.
  • Version.TryParse
  • Semaphore.TryOpenExisting

Try- methods that return a struct wrapper around a reference type thatll be non-null if returning true

  • MemoryMarshal.TryGetArray(…, out ArraySegment)

Interfaces where T should be nullable on some methods but non-nullable on others, e.g.

  • IEqualityComparer: Equals(T x, T y) should have nullable arguments, but GetHashCode(T obj) should have a non-nullable argument

Fields/properties of type T that start life as default(T), and maybe become default(T) again

  • AsyncLocal.Value
  • ThreadLocal.Value
  • StrongBox.Value

Array slots that need to be settable to default(T)

  • Pretty much every collection we have, nulling out a slot when an element is removed

Methods where whether the return value can be null is impacted by whether an argument is null

  • RegistryKey.GetValue
  • Path.GetFullName
  • Path.ChangeExtension
  • Path.GetExtension
  • Path.GetFileNameWithoutExtension
  • Convert.ToString(string?)
  • Delegate.Combine

Methods where whether the return value can be null is impacted by whether an argument is true/false

  • Type.GetType(…, bool throwOnError)

Methods where one argument will be non-null if another is non-null

  • Volatile.Write
  • Interlocked.Exchange
  • Interlocked.CompareExchange (this ones more complicated still)

Methods that accept an delegate<object?> and object?, but where the argument to Action<object?> will be non-null iff the object? is non-null

  • Task.Factory.StartNew
  • ExecutionContext.Run
  • SynchronizationContext.Post
  • CancellationToken.Register
  • ThreadPool.QueueUserWorkItem
  • Task ctor
  • IValueTaskSource.OnCompleted

Ref arguments that will be non-null upon return

  • Array.Resize(ref T[]);
  • LazyInitializer.EnsureInitialized(ref T) (this ones complicated if T is a nullable value type)

The first question is whether or not the annotation of the method changes, e.g.

public static class Enumerable
{
    [return: MaybeNull]
    public static T FirstOrDefault<T>(this IEnumerable<T> src) {...}

    public static T Identity<T>(T t) => t;
}

Here FirstOrDefault<string> does not produce a string?, but the flow state of values returned from calls is a string with a MaybeNull state. Following through, for Identity(FirstOrDefault<string>(...)), the inferred type of T for Identity<T> would be string? because the flow state is used to construct the annotation of the substituted type argument.

As written, if you were to provide a FirstOrDefault<string> override for an analogous FirstOrDefault<T> instance method, you could not write string? for the return type, despite the MaybeNull attribute.

The proposal does not attempt to address modifying what the methods accept, only what they produce, meaning that providing MaybeNull on a parameter does not indicate that the input can be nullable.

However, we think MaybeNull may be common or impactful enough to consider syntax, like allowing T? on an unconstrained type parameter or T : object?, to express MaybeNull. Importantly, this would not address all the patterns we've found while annotating CoreFX, like Interlocked.CompareExchange or Debug.Assert.

The other problem is that the list of attributes which are needed to represent all patterns is unbounded for all the code, but even the set of attributes needed for just what we know is common is quite large.

Conclusion

We think the best approach right now is to try to address ~80% of the common cases using a mixture of attributes and special casing for extremely complicated contracts like Interlocked.CompareExchange.

For MaybeNull and T? specifically, we are conflicted between the potential value that T? can provide, especially in annotating nested types, and the additional complexity in both language and implementation. One thing we're particularly worried about is the design work in adopting T? and revisiting some previous decisions. We're going to examine that work and if it's low in comparison to the MaybeNull attribute work, there's substantial support for implementing the feature. If we do, implement T?, we do not want to include the MaybeNull, specifically.