Lifted Conversions
These apply to nullable value types that were introduced in .NET 2. Prior to this reference types were obviously nullable but value types, like int, long, double, decimal, etc, were NOT nullable making it hard to figure out if a value had been set or not without resorting to initializing the value types with some "magic value" like -1. The language designers for .NET implemented the Nullable construct as a struct that wrapped a base value type, T, and conveniently there is a generic constraint to restrict T to be a value type [ where T: struct]. Now, rather than re-implement all the implicit conversions between value types for nullable value types they adopted a policy of lifted conversions from the underlying base value types. This is best illustrated with some some code doing implicit conversions:

    1 int i = 1;

    2 long j = 1;

    3 

    4 int? ni = 1;

    5 long? nl = 1;

    6 

    7 j = i;      // fine

    8 nl = ni;    // fine

    9 

   10 i = j;      // Compile error - downcasts need to be explicit

   11 ni = nl;    // Compile error - downcasts need to be explicit

   12 

   13 ni = i;     // fine

   14 ni = j;     // Compile error - no implicit cast from long to int?

Line 10 indicates the expected case where a downcast from a long to an int must be explicit thus this line causes a compilation error. Line 11 is attempting to do an implicit cast from a long? to an int? and because of the lack of an implicit cast from a long to an int, it too causes a compilation error. This is in stark contrast to Lines 7 and 8 where the opposite cast is being attempted, int to long and int? to long?. Line 7 works as expected because it is a widening conversion, and Line 8 works because a lifted conversion (from int to long) is being used.

Line 13 works fine too because a non-nullable value type has a narrower range of values than it's corresponding nullable value type, so a cast from T? to T is considered safe and can be done implicitly.

Note that lifted conversions are applicable not just to the predefined value types but also to any implicit conversions defined on user-defined value-types (structs). These can be defined using code such as this...

public struct Rotation

{

    public int Degrees { get; set; }

 

    // to implicitly cast an int to a Rotation

    public static implicit operator Rotation(int value)

    {

        return new Rotation { Degrees = value % 360 };

    }

}

Lifted Operators
Along similar lines as the lifted conversions on nullable value types, lifted operators allow predefined and user-defined operators that work for non-nullable value types also to work for nullable forms of those types. In other words, operators defined on T are available to T? where T is a value type (struct).

I've often heard programmers allude to this as a form of inheritance but that isn't correct. Both user-defined and predefined operators and conversions are defined as static methods (see below) and as such are not inherited. It's because of this that the concept of lifted operators is employed. Consider this simple value type with the addition operator overloaded:

public struct Rotation

{

    public int Degrees { get; set; }

 

    public static Rotation operator +(Rotation a, Rotation b)

    {

        return new Rotation {Degrees = (a.Degrees + b.Degrees)%360};

    }

}

When someone comes along after you and creates a Rotation? your overload of the + operator effectively becomes this:

    // lifted operator for Rotation? is equivalent to...

    public static Rotation? operator +(Rotation? a, Rotation? b)

    {

        if (a == null || b == null )

            return null;

 

        return new Rotation?

                    {

                        Degrees = (a.Value.Degrees + b.Value.Degrees) % 360

                    };

    }

Effectively it does what you'd expect with the bonus null propagation logic added.

There are more subtleties involved in this (like the qualifying criteria for operators to be lifted, and what the type of the return value, if any, is for the lifted operator), so best consult Section 24.3.1. of the C# Language Specification. Note that Eric Lippert has admitted that the specification is a bit inconsistent with it's use of the term "lifted".

Bookmark and Share