FPGA
FPGA

Reputation: 3875

Does the compiler interpret the following snippets the same way?

Consider

public List<GroupByDateType> GroupByItems
{
    get { return Enum.GetValues(typeof(GroupByDateType)).Cast<GroupByDateType>().ToList(); }
}

And

private List<GroupByDateType> _GroupByItems;
public List<GroupByDateType> GroupByItems
{
    get {return _GroupByItems??(_GroupByItems=Enum.GetValues(typeof(GroupByDateType)).Cast<GroupByDateType>().ToList());}
}

Usually I go with the second one because when I look at the first one it seems to me that the list is regenerated, but is that really what happens, does the CLR create the backing field for me by any chance?

Upvotes: 2

Views: 105

Answers (3)

Jon Hanna
Jon Hanna

Reputation: 113352

No. Though the both the compiler and the jitter are free to perform all manner of optimisations, as a rule the compiler only performs those that are most obvious (in particular, removing dead code), while the jitter tends not to do anything which would add to the layout or have a downside the developer chose to avoid.

Memoisation changes the layout by adding a field, and optimises for speed at the cost of memory and so is something you don't want to happen every time (if you consider the number of repeat calls times the cost of calculating to not be worth the extra memory used). Among other things, deciding on that balance involves factors outside of just this code, such as how many such objects will be created (and hence how much memory used). It wouldn't be a good automatic change across a large number of cases.

There's also the fact that memoisation is easy; if you'd wanted it, you'd have done it, if you didn't want it you wouldn't have (and indeed, certainly shouldn't every time, only when it has a real advantage).

Conversely, memorisation that covers null as a possible result is slightly trickier, and that which must be threadsafe trickier again (there's a few different approaches that work in different concurrent cases, all of which are easy in themselves, but picking which to use often isn't). It would be totally inappropriate for an "optimisation" to turn a method (including a property getter) that was threadsafe into one that was not, or at least changed the behaviour of the object in the face of concurrent calls.

Memoisation can also sometimes have subtle semantic differences such as your example; the behaviour differs if calling code changes the list. This could be vital (the list is examined for such changes by other code) or a bug (the next caller gets a changed list that is incorrect) or irrelevant (it's internal-only, and you don't change it anywhere so you're cutting out the cost of safeguarding). Not all memoisation has this issue (one could return a read only list, for example), but it does mean it couldn't be done automatically.

Upvotes: 4

MarcinJuraszek
MarcinJuraszek

Reputation: 125660

No, there is not backing field created, because property is not an auto-implemented property.

private List<GroupByDateType> _GroupByItems;
public List<GroupByDateType> GroupByItems
{
    get { return _GroupByItems ?? (_GroupByItems = Enum.GetValues(typeof(GroupByDateType)).Cast<GroupByDateType>().ToList()); }
}

public List<GroupByDateType> GroupByItems2
{
    get { return Enum.GetValues(typeof(GroupByDateType)).Cast<GroupByDateType>().ToList(); }
}

If you try to check generated IL you'll see only one backing field, the one you defined in second version of the property:

enter image description here

The getter only calls all specified methods, without storing results somewhere:

.method public hidebysig specialname 
    instance class [mscorlib]System.Collections.Generic.List`1<valuetype Program/GroupByDateType> get_GroupByItems2 () cil managed 
{
    // Method begins at RVA 0x2052
    // Code size 26 (0x1a)
    .maxstack 8

    IL_0000: ldtoken Program/GroupByDateType
    IL_0005: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_000a: call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type)
    IL_000f: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::Cast<valuetype Program/GroupByDateType>(class [mscorlib]System.Collections.IEnumerable)
    IL_0014: call class [mscorlib]System.Collections.Generic.List`1<!!0> [System.Core]System.Linq.Enumerable::ToList<valuetype Program/GroupByDateType>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
    IL_0019: ret
} // end of method Program::get_GroupByItems2

where for the one with a field you'll get additional assignment and null check:

.method public hidebysig specialname 
    instance class [mscorlib]System.Collections.Generic.List`1<valuetype Program/GroupByDateType> get_GroupByItems () cil managed 
{
    // Method begins at RVA 0x2070
    // Code size 45 (0x2d)
    .maxstack 3
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<valuetype Program/GroupByDateType> CS$0$0000
    )

    IL_0000: ldarg.0
    IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<valuetype Program/GroupByDateType> Program::_GroupByItems
    IL_0006: dup
    IL_0007: brtrue.s IL_002c

    IL_0009: pop
    IL_000a: ldarg.0
    IL_000b: ldtoken Program/GroupByDateType
    IL_0010: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_0015: call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type)
    IL_001a: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::Cast<valuetype Program/GroupByDateType>(class [mscorlib]System.Collections.IEnumerable)
    IL_001f: call class [mscorlib]System.Collections.Generic.List`1<!!0> [System.Core]System.Linq.Enumerable::ToList<valuetype Program/GroupByDateType>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
    IL_0024: dup
    IL_0025: stloc.0
    IL_0026: stfld class [mscorlib]System.Collections.Generic.List`1<valuetype Program/GroupByDateType> Program::_GroupByItems
    IL_002b: ldloc.0

    IL_002c: ret
} // end of method Program::get_GroupByItems

Upvotes: 2

Nico
Nico

Reputation: 12683

NO, the compiler will treat those two statements completely different.

In the first statement you are re-evalute the List<T> on every property get request. In the second statement you are essentially writing this (in shorthand).

private List<GroupByDateType> _GroupByItems;
public List<GroupByDateType> GroupByItems
{
    get 
    {
        if(_GroupByItems != null)
            return _GroupByItems;
        _GroupByItems=Enum.GetValues(typeof(GroupByDateType)).Cast<GroupByDateType>().ToList());
        return _GroupByItems;
    }
}

If you review the re-written sample this is how your one liner would execute.

Basically with the double question marks (??) null coalescing operator the compiler will execute the code after the ?? if the instance of _GroupByItems (the object or statement before the ??) is null.

If the _GroupByItems instance is not null then the _GroupByItems is returned. It is also worth noting that the second statement (after the ??) is actually setting the instance of _GroupByItems so on the next call the instance of the object is returned.

Finally, as the instance is set all subsequent calls will use the instance of the List and will no re-evaluate the list. This means if the list changes (although this example it wouldnt as they are enums) you must set the instance _GroupByItems to null (or the new instance of the list).

Hope this helps.

Upvotes: 3

Related Questions