简体   繁体   中英

Unbounded type parameters in Generics C#

I am new to Generics I started learning generics from MSDN Library

I am unable to understand below points about unbounded type parameters.

Type parameters that have no constraints, such as T in public class SampleClass<T>{} , are called unbounded type parameters. Unbounded type parameters have the following rules:

  • The != and == operators cannot be used because there is no guarantee that the concrete type argument will support these operators.
  • You can compare to null . If an unbounded parameter is compared to null , the comparison will always return false if the type argument is a value type.

I did not find any example of above points. It will be great if somebody give me example to understand the points.

Note: My question is about the use of != and == operators ... why we can't use those operators in unbounded type and why always return false if Unbounded parameter is compared to null

Lets assume for a second this was possible:

public class C
{
     public bool AreEqual<T>(T first, T second)
     {
           return first == second;
     }
}

Now assume a struct called F, which has no implementation of the == operator (which no struct has by default). What should happen here?

F first = new F();
F second = new F();
var c = new C();
Console.WriteLine(c.AreEqual(first, second));

The value type has no implementation of == , and we can't defer to Object.ReferenceEquals since this is, again, a value type. That is why the compiler doesn't let you do this.

Type parameters can have constraints, for example: where T: BaseClass . It means that T must be inherited from BaseClass . If there is no such constraints for a type parameter it called unbounded.

There's 2 points from the documentation that you cited:

The != and == operators cannot be used because there is no guarantee that the concrete type argument will support these operators.

This means that the compiler can't know that the type T has the operators == and != . A SampleClass<int> would work, but SampleClass<MyType> might not if MyType didn't implement the operators. Since T is unbounded, it means that there is no way for the compiler to know what is expected, so it has to take the most restrictive case.

You can compare to null. If an unbounded parameter is compared to null, the comparison will always return false if the type argument is a value type.

This just points out that you can compare to null, but if T is a non-nullable type, then it always returns false. Consider the following:

int i = 0;
if (null == i)
{
}

This will generate a compiler warning, since there's no value you can give to i to make the expression true .

My question is about the use of != and == operators ... why we can't use those operators in unbounded type

What would they mean? Normally != and == means "not equal to" and "equal to" but the precise meaning of that depends on whether they are value types or reference types, (and whether they have overloaded those operators, but that also doesn't apply to many bounded types). Without constraint to at least be one of those != and == have no meaning.

and why always return false if Unbounded parameter is compared to null.

You are mis-reading. What is actually said, and quoted by you earlier, is:

the comparison will always return false if the type argument is a value type . [Emphasis mine]

This is actually incorrect, a nullable value type can return true in this case:

public class Test<T>
{
  public bool IsNull(T val)
  {
     return val == null;
  }
}

With the above code, we get true if we call new Test<int?>().IsNull(null) and false if we call new Test<int?>().IsNull(1) .

With any value type other than Nullable types, we get false because that's the only possible value; value types other than Nullable<T> cannot be null.

It's worth noting that the jitter will pre-empt this, in that when it produces machine code for the method it will know that val == null is always false and replace the code with the constant false . If there is a branch then it need not be jitted. Consider:

public string CallToString(T val)
{
  if (val == null)
    return null;
  else
    return val.ToString();
}

When this is jitted for a T that is a non-nullable value type, then it's the same as if the code had been:

public string CallToString(T val)
{
  return val.ToString();
}

Because the jitter knows the first branch can never be hit. Similarly consider Enumerable.Max() . This method returns null on empty sequences that are of a nullable type, and throws an exception otherwise. For the specific overrides this is simple: Enumerable.Max(this IEnumerable<decimal?> source) for example has the code to return null in this case and Enumerable.Max(this IEnumerable<decimal> source) the code to throw. For the generic case though, it needs to cover both cases. It does so thus:

public static TSource Max<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = default(TSource);
  if (value == null)
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      do
      {
        if (!e.MoveNext()) return value;
        value = e.Current;
      } while (value == null);
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (x != null && comparer.Compare(x, value) > 0) value = x;
      }
    }
  }
  else
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      if (!e.MoveNext()) throw Error.NoElements();
      value = e.Current;
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (comparer.Compare(x, value) > 0) value = x;
      }
    }
  }
  return value;
}

When jitted for a nullable type (reference type of Nullable<T> the jitter knowing in advance that default(TSource) == null is always true means it's the same as if it was jitting:

public static TSource Max<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = null;
  using (IEnumerator<TSource> e = source.GetEnumerator())
  {
    do
    {
      if (!e.MoveNext()) return value;
      value = e.Current;
    } while (value == null);
    while (e.MoveNext())
    {
      TSource x = e.Current;
      if (x != null && comparer.Compare(x, value) > 0) value = x;
    }
  }
  return value;
}

While if the type is aa non-nullable value type then it's the same as if it was jitting:

public static TSource Max<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = default(TSource);
  using (IEnumerator<TSource> e = source.GetEnumerator())
  {
    if (!e.MoveNext()) throw Error.NoElements();
    value = e.Current;
    while (e.MoveNext())
    {
      TSource x = e.Current;
      if (comparer.Compare(x, value) > 0) value = x;
    }
  }
  return value;
}

As such the fact that == between a non-nullable value type and null is always false (and != always true ) isn't just a restriction, it can actually be useful in allowing us to cover nullable and non-nullable types differently, and the jitter will behave sensibly in removing the branch that isn't used in a particular case.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM