Difficult about simple or features of Linq to objects

    LINQ to objects has now firmly entered our lives, taking victorious steps across the entire stack of .net applications. In this article, I would like to give examples of several not obvious things that I had to deal with when working with LINQ. Interesting - please, under cat.

    1. Feature of working with List


    LINQ differs in its implementation when an object of type IEnumerable comes to it. and when List comes. Even the simple Where method creates different iterators:
    1. if (source is Iterator) return ((Iterator)source).Where(predicate);
    2. if (source is TSource[]) return new WhereArrayIterator((TSource[])source, predicate);
    3. if (source is List) return new WhereListIterator((List)source, predicate);
    * This source code was highlighted with Source Code Highlighter.

    By the way, note that the array will also have its own iterator, the difference of which is that it will receive the elements directly through the inverse of the index.

    What makes this feature interesting for us? Well, for example, I was usually skeptical about calling the Last construct , because according to the idea, it should go through the entire enumerator to the end. However, if you apply it to an object that implements the IList interface, then the last element will be obtained through the indexer, which of course is not so bad.
    1.  public static TSource LastOrDefault(this IEnumerable source) {
    2.       if (source == null) throw Error.ArgumentNull("source");
    3.       IList list = source as IList;
    4.       if (list != null) {
    5.         int count = list.Count;
    6.         if (count > 0) return list[count - 1];
    7.       }
    8.       else {
    * This source code was highlighted with Source Code Highlighter.

    Similar optimization is done for methods of type Single
    1.  public static TSource Single(this IEnumerable source) {
    2.       if (source == null) throw Error.ArgumentNull("source");
    3.       IList list = source as IList;
    4.       if (list != null) {
    5.         switch (list.Count) {
    6.           case 0: throw Error.NoElements();
    7.           case 1: return list[0];
    * This source code was highlighted with Source Code Highlighter.


    2. The work of GroupBy


    Let's say we have a code like this:
    1. class Key
    2. {
    3.   private readonly int _number;
    4.   public static int HashCallsCount;
    5.   public static int EqualsCallsCount;
    6.  
    7.   public Key(int number)
    8.   {
    9.     _number = number;
    10.   }
    11.  
    12.   public bool Equals(Key other)
    13.   {
    14.     if (ReferenceEquals(null, other))
    15.     {
    16.       return false;
    17.     }
    18.     if (ReferenceEquals(this, other))
    19.     {
    20.       return true;
    21.     }
    22.     return other._number == _number;
    23.   }
    24.  
    25.   public override bool Equals(object obj)
    26.   {
    27.     EqualsCallsCount++;
    28.     if (ReferenceEquals(null, obj))
    29.     {
    30.       return false;
    31.     }
    32.     if (ReferenceEquals(this, obj))
    33.     {
    34.       return true;
    35.     }
    36.     if (obj.GetType() != typeof (Key))
    37.     {
    38.       return false;
    39.     }
    40.     return Equals((Key) obj);
    41.   }
    42.  
    43.   public override int GetHashCode()
    44.   {
    45.     HashCallsCount++;
    46.     return _number;
    47.   }
    48. }
    49.  
    50. class Test
    51. {
    52.   public int Number { get; set; }
    53.   public string Name { get; set; }
    54.  
    55.   public Key Key
    56.   {
    57.     get { return new Key(Number);}
    58.   }
    59.  
    60.   public override string ToString()
    61.   {
    62.     return string.Format("Number: {0}, Name: {1}", Number, Name);
    63.   }
    64. }
    65. internal class Program
    66. {
    67.   private static void Main(string[] args)
    68.   {
    69.  
    70.     var items = Enumerable.Range(1, 20).Select(x => new Test {Number = x%3});
    71.  
    72.     foreach (var group in items.GroupBy(x=>x.Key))
    73.     {
    74.       Console.WriteLine("Group key: {0}", group.Key);
    75.       foreach (var item in group)
    76.       {
    77.         Console.WriteLine(item);
    78.       }
    79.     }
    80.  
    81.     Console.WriteLine("EqualsCallsCount {0}", Key.EqualsCallsCount);
    82.     Console.WriteLine("HashCallsCount {0}", Key.HashCallsCount);
    83.   }  
    84. }
    * This source code was highlighted with Source Code Highlighter.

    What value will be displayed for the EqualsCallsCount and HashCallsCount variables ? 17 and 20, respectively. 17 - as values ​​that became group keys will not be compared with other keys (just in case, I will indicate that the group in this example is exactly 3). 20 is a challenge, because the hashcode from the Key object is used to determine which group to place the Test object in . Here it should be noted that if the hashcode stops giving unique values ​​(for example, it always returns 0), then the number of calls to Equals will increase to 38. I think the reasons are clear. Another interesting detail is that the default array of groups is 7
    1. Lookup(IEqualityComparer comparer) {
    2.       if (comparer == null) comparer = EqualityComparer.Default;
    3.       this.comparer = comparer;
    4.       groupings = new Grouping[7];
    5.     }
    * This source code was highlighted with Source Code Highlighter.

    And then it increases linearly, copying the entire array at this step.
    1.  void Resize() {
    2.       int newSize = checked(count * 2 + 1);
    3.       Grouping[] newGroupings = new Grouping[newSize];
    4.       Grouping g = lastGrouping;
    5.       do {
    6.         g = g.next;
    7.         int index = g.hashCode % newSize;
    8.         g.hashNext = newGroupings[index];
    9.         newGroupings[index] = g;
    10.       } while (g != lastGrouping);
    11.       groupings = newGroupings;
    12.     }
    * This source code was highlighted with Source Code Highlighter.

    Because you cannot specify capacity for the GroupBy method , it is better to refrain from grouping with a large number of keys, otherwise I may have problems with memory (well, of course, the speed will drain).

    3. An enumerator is also an object.


    Let's consider two functions:
    1. private static IEnumerable SortTest(Func> list)
    2.   {
    3.     foreach (var item in list().OrderBy(x => x.Number).ThenBy(x => x.Name))
    4.     {
    5.       yield return item;
    6.     }
    7.   }
    * This source code was highlighted with Source Code Highlighter.

    and
    1. private static IEnumerable SortTest2(Func> list)
    2.   {
    3.     return list().OrderBy(x => x.Number).ThenBy(x => x.Name);
    4.   }
    * This source code was highlighted with Source Code Highlighter.

    We assume that list () returns a stable list of elements, such as an array. Are functions equivalent? Of course not (by the way in the last resharper there is a corresponding bug ). In the first case, the list () function will always be called when we go through the returned enumerator. And accordingly, if we begin to return other values, then the values ​​of the enumerator will become different. In the second case, the list () function will be called only once, its result will be stored in the source field of the OrderedEnumerable class and in the future, no matter how much we go through the returned enumerator, the values ​​in it will be the same.

    Prisoners


    I hope this short article will be useful for someone and will help to avoid errors when working with Linq to object. Comments and additions are welcome.

    References


    MSDN o LINQ

    Thank you for your attention.

    Also popular now: