11

I've noticed a strange VB.NET thing. Coming from this question I've provided a way to access keys and values of dictionaries' KeysCollection and ValuesCollection via index to get, say, the first item. I know that it makes only sense in a SortedDictionary since a normal Dictionary is not ordered (well, you should not rely on its order).

Here's a simple example:

Dim sortedDict As New SortedDictionary(Of DateTime, String)
sortedDict.Add(DateTime.Now, "Foo")

Dim keys As SortedDictionary(Of DateTime, String).KeyCollection = sortedDict.Keys
Dim values As SortedDictionary(Of DateTime, String).ValueCollection = sortedDict.Values
Dim firstkey As DateTime = keys(0)
Dim firstValue As String = values(0)

But I was surprised that the question's asker said that it doesn't compile whereas it compiles and works for me without a problem:

System.Diagnostics.Debug.WriteLine("Key:{0} Value:{1}", firstkey, firstValue) ' Key:04/29/2016 10:15:23 Value:Foo

So why can I use it like there was an indexer if there isn't actually one in SortedDictionary(Of TKey, TValue).KeyCollection-class and also none in the ValueCollection. Both implement ICollection<T> which is the parent interface of IList<T>. So you can loop it and it has a Count property, but you can't access items via index as I do above.

Note that it's a fresh console application with no extensions inside. I can't go to the definition of the indexer either(also not with resharper). Why does it work for me?

Side-note: it doesn't work in C#. I get the expected compiler error:

Cannot apply indexing with [] to an expression of type 'SortedDictionary.KeyCollection'

var dict = new SortedDictionary<DateTime, string>();
dict.Add(DateTime.Now, "Foo");
DateTime dt = dict.Keys[0]; // here

Here's a screenshot of the compiling VB.NET code:

enter image description here

Community
  • 1
  • 1
Tim Schmelter
  • 450,073
  • 74
  • 686
  • 939
  • 1
    If in VB.NET project settings you [remove `System.Linq` from `Imported Namespaces`](http://i.stack.imgur.com/0y0mW.png), you will get the same compiler error as in C#. Conclusion: VB.NET implicitly calls GetEnumerator. – GSerg Apr 29 '16 at 08:44
  • @GSerg: since when? Is that new in VS 2015? Imo that's a very bad decision. You don't even notice that you are enumerating a large sequence to find a given index. But thanks for the hint, you're right. They should do that only with `Option Strict Off`(or a new compiler hint). – Tim Schmelter Apr 29 '16 at 08:48
  • Same happens in VS 2008. (Why did I change it to `GetEnumerator` by the way? I meant `ElementAt` and now cannot edit it back. Though I'm not sure exactly what it calls.) – GSerg Apr 29 '16 at 08:52
  • Disassembly shows `ElementAtOrDefault`. – GSerg Apr 29 '16 at 08:54
  • 1
    @GSerg: yep, that is a duplicate even if the title is somewhat misleading and only [John Saunders answer](http://stackoverflow.com/a/788906/284240) contains the `System.Linq`-hint as `Edit2`. But still not clear where this is documented and why `ElementAtOrDefault` is taken if i try to use invalid code(using a non-existing indexer). – Tim Schmelter Apr 29 '16 at 09:03

2 Answers2

8

It invokes Enumerable.ElementAtOrDefault, not the indexer.

// [10 13 - 10 31]
IL_001f: ldloc.1      // keys
IL_0020: ldc.i4.0     
IL_0021: call         !!0/*valuetype [mscorlib]System.DateTime*/ [System.Core]System.Linq.Enumerable::ElementAtOrDefault<valuetype [mscorlib]System.DateTime>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0/*valuetype [mscorlib]System.DateTime*/>, int32)
IL_0026: stloc.2      // firstKey

This behavior is documented in the Visual Basic Language Specification, 11.21.3:

Every queryable collection type whose element type is T and does not already have a default property is considered to have a default property of the following general form:

Public ReadOnly Default Property Item(index As Integer) As T
    Get
        Return Me.ElementAtOrDefault(index)
    End Get
End Property

The default property can only be referred to using the default property access syntax; the default property cannot be referred to by name. For example:

Dim customers As IEnumerable(Of Customer) = ...
Dim customerThree = customers(2)

' Error, no such property
Dim customerFour = customers.Item(4)

If the collection type does not have an ElementAtOrDefault member, a compile-time error will occur.

GSerg
  • 76,472
  • 17
  • 159
  • 346
Cheng Chen
  • 42,509
  • 16
  • 113
  • 174
  • Thanks. Is that documented somewhere? – Tim Schmelter Apr 29 '16 at 08:57
  • 2
    @TimSchmelter Apparently it is: [Visual Basic Language Specification](https://msdn.microsoft.com/en-us/library/ms234437.aspx), 11.21.3 (credits go to [Stephen Cleary](http://stackoverflow.com/users/263693/stephen-cleary) at https://social.msdn.microsoft.com/Forums/en-US/36492946-8560-4b03-8607-4a47a2b569d2/no-default-property-for-linked-list-in-csharp-when-its-there-for-vb-?forum=netfxbcl) – GSerg Apr 29 '16 at 09:03
0

There is a considerable performance cost when we use Enumerable.ElementAtOrDefault or Enumerable.ElementAt. Unless the source implements IList(of T) interface, Linq does not have shorter route to get to the element at the specified index. So it iterates each element untill iteration count reaches to value of specified index. No magic. Enumerable. Count() has the same story except in this case ICollection interface if implemented by source, Linq grabs it otherwise iteration untill the last element is required to produce Count. I don't know why Vb.net allowing it implicitly because chances are that cases like this will remain unnoticed until I face serious performance issue. Dictionary only implements ICollection not IList. I think one needs to be careful with Vb.net as it is not as strictly typed language as C#.

Mukesh Adhvaryu
  • 642
  • 5
  • 16
  • 1
    You're right, this auto implemented magic code is a terrible source of performance issues. In most cases you wouldn't notice it. But if it's causing performance issues it's very difficult to find the reason. But it has nothing to do with VB.NET being strictly typed or not. You get this auto implemented indexer on collection types even if you have `Option Strict On`. The only way to avoid it is to avoid using an indexer on types which don't implement `IList`/`IList(Of T)`. So you already have to have notice of this "auto-indexer". – Tim Schmelter Apr 29 '16 at 13:51
  • Now if this is the case, then it really sparks the debate why C# over VB.net. – Mukesh Adhvaryu Apr 29 '16 at 14:11
  • While it is a potential source of performance problems, it only kicks in when there is no faster way to do it anyway. [Here](http://stackoverflow.com/a/36918491/11683) @TimSchmelter used `foreach` to return the first key - the implicit `ElementAtOrDefault` would do just that, but... implicitly. If you *have to* retrieve something by index from a collection that does not support it, you will end up using `ElementAtOrDefault` or writing a conceptually identical method. VB just does that for you, it's sort of its way. – GSerg Apr 29 '16 at 17:22
  • There's a great chance that the language does something that causes performance problems without letting you know it. Even worse, the indexer appears to be very efficient so that everyone uses it, maybe even without storing the result in a variable. The cause of this issue is also hidden. Very bad decision to auto magically provide indexers. – Tim Schmelter Apr 30 '16 at 01:07