3

I have some code, and it throws. I don't understand why.

string s="asdf";
Console.WriteLine(s[1..3]);

dynamic d="asdf";
Console.WriteLine(d[1..3]); // throws 
// RuntimeBinderException: The best overloaded method match for 'string.this[int]' has some invalid arguments

Is there some statically resolved compiler magic? The generated IL suggests this.

callvirt    System.String.Substring

Is there any way to use range indexing on dynamically declared expressions?

recursive
  • 83,943
  • 34
  • 151
  • 241
  • 3
    My guess is that `dynamic` is not `Countable` - https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/ranges#implicit-range-support . – mjwills May 19 '20 at 02:20
  • "A type is ***Countable*** if it has a property named `Length` or `Count` with an accessible getter and a return type of `int`". I don't see why it wouldn't qualify. – recursive May 19 '20 at 02:43

2 Answers2

3

Range Indexing is released in C# 8.0 and it doesn't support for dynamic, it cannot be translated to another code (in this case is text.SubString()) which means cannot resolve at runtime. I encountered the same problem with dynamic in Lambda Tuple C# 8.0 also.

You might check how this translation work on the right side below code.

public class MyClass {
    public class RangeIndexer
    {
        public string MyString { get; set; }
        public char this[int index] { get => MyString[index]; }
        public string this[Range range] { get => MyString[range]; }
    }

    public void Main() {
        string s = "asdf";
        Console.WriteLine(s[1..3]); // Translate to text.SubString()

        dynamic d = "asdf";
        Console.WriteLine("Index: " + d[1]); // Address to this[int]
        //Console.WriteLine("Range1: " + d[1..3]); // Cannot translate to text.SubString() => Crashed
        Console.WriteLine("Range2: " + d.Substring(1, 2)); // Local method of string
        Console.WriteLine("Range3: " + $"{d}"[1..3]); // Cast as string and translate like Range1

        dynamic rangeIndexer = new RangeIndexer();
        rangeIndexer.MyString = "asdf";
        Console.WriteLine("Range4: " + rangeIndexer[1..3]); // Address to this[range]
    }
}

The range indexing is translated to substring() by IDE during compilation so it's not actually implemented in string class, therefore it's explained why only single indexing d[1] worked because it's declared.

enter image description here

So in short, we have 2 options

Option 1: In fact, dynamic is using Reflection technical to resolve and grab if it's in scope of the variable, method,.. that means no more translation codes will occur in Reflection. Thus, casting dynamic to specific type would help IDE translate them basically.

Option 2: Object should implement like RangeIndexer class to work as dynamic type which make sure the Reflection could grab it. But almost classic types don't support so it only works with your own model.

Pavel Anikhouski
  • 21,776
  • 12
  • 51
  • 66
Tấn Nguyên
  • 1,607
  • 4
  • 15
  • 25
2

To extend an existing answer

Is there some statically resolved compiler magic?

Yes, there is.

Indexer with range (this[System.Range] or this[start..end]) is supported in two ways: explicitly and implicitly.

Explicitly. If type declares indexer this[System.Range] then this indexer is used whenever we use notation this[start..end]. Compiler converts an expression start..end to the appropriate System.Range object and generates code that calls indexer this[System.Range].

Implicitly. If type does not contain indexer this[System.Range] it could support range indexing implicitly. To be able to support range indexing implicitly type must satisfy next conditions:

  • It must contain Length or Count property.
  • It must contain Slice(start, end) method with two int parameters. Type string is a special case; for string type method Substring is used instead of Slice.

If type satisfies these conditions then when we use notation this[start..end] compiler generates code that calls method Slice(start, end) (or Substring(start, end) for string).

For more details see C# specification: Ranges.


So string type supports range indexing implicitly.

Consider the next code sample:

dynamic d = "asdf";
Console.WriteLine(d[1..3]);

It is impossible to determine during compilation that dynamic refers to the object that supports range indexing. Therefore complier does not generate code that calls method Substring. Instead it generates code that calls indexer this[System.Range]. string type does not contain such indexer, therefore RuntimeBinderException is generated.

Iliar Turdushev
  • 4,935
  • 1
  • 10
  • 23
  • I'm pleased to learn that there is at least a language specification proposal. For some reason, I though MS stopped producing these around C# 5. – recursive May 19 '20 at 13:14
  • @recursive Currently there is a [draft C# 6 specification](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/introduction) and [specification proposals for C# 7 and C# 8](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-7.0/pattern-matching). – Iliar Turdushev Jun 05 '20 at 06:12