Josiah Ruddell
Josiah Ruddell

Reputation: 29841

C# 4.0 Implicitly Typed Dynamic Objects

Data File: (Data.txt) lines represent width height

5
6 9
7 2
4 4

C# Code:

var list = new List<dynamic>();
using (var sr = new StreamReader("Data.txt", Encoding.UTF8))
{
    list = sr.ReadToEnd().Split('\n').Select(r =>
    {
        var split = r.Split(' ');
        var len = split.Length;
        return new {
            w = len > 0 ? int.Parse(split[0].Trim()) : 0,
            h = len > 1 ? int.Parse(split[1].Trim()) : 0 
        } as dynamic;
    }).ToList();
}
int Area = list.Sum(r => r.h * r.w);

The example works as is. I had to do a few undesired things to make it work.

First I had to declare the list to avoid the using scope - since I do not have a typed dimension object I made the type dynamic (var list = new List<dynamic>()).

The undesirable part is casting the anonymous object to a dynamic (as dynamic). Otherwise I get

Cannot implicitly convert type System.Collections.Generic.List<AnonymousType#1> to System.Collections.Generic.List<dynamic>

Why do I get this error? I know a dynamic can hold an anonymous type, so is this a problem with the ToList() extension and dynamics?

I need to be able to access the anonymous list items outside of the using statement, as in the last line that calculates area.


Solution: I went with dtb's answer. It avoided the use of a using statement and dynamics all together. Thank you all for the input!

var list = 
    (from line in File.ReadLines("Data.txt")
    let parts = line.Split(' ')
    let width = int.Parse(parts[0])
    let height = parts.Length > 1 ? int.Parse(parts[1]) : 0
    select new { width, height }).ToList();

Upvotes: 6

Views: 7711

Answers (7)

Dale Anderson
Dale Anderson

Reputation: 1711

I mostly agree with other remarks, dynamic is probably not the go here, but for discussion, but as Albin points out, it is to do with generic parameters being invariant unless explicitly specified with the 'in' or 'out' modifiers.

You can make your code work by adding .Cast() before the call to .ToList()

Upvotes: 0

ChaosPandion
ChaosPandion

Reputation: 78292

You really should do something like this:

private IEnumerable<Tuple<int, int>> ReadFile(string filePath, 
    Encoding encoding)
{
    using (var sr = new StreamReader(filePath, encoding))
    {
        string line;
        while ((line = sr.ReadLine()) != null) 
        {
            var split = line.Split(' ');
            var w = split.Length > 0 ? int.Parse(split[0]) : 0;
            var h = split.Length > 1 ? int.Parse(split[1]) : 0;
            yield return Tuple.Create(h, w);
        }
    }
}

Now you have a lazy and strongly typed sequence of values.

Upvotes: 3

dtb
dtb

Reputation: 217401

You can use File.ReadLines to avoid the StreamReader.

IEnumerable<dynamic> query =
    from line in File.ReadLines("Data.txt")
    let parts = line.Split(' ')
    let width = int.Parse(parts[0])
    let height = parts.Length > 1 ? int.Parse(parts[1]) : 0
    select new { width, height } as dynamic;

List<dynamic> list = query.ToList();

int area = list.Sum(t => t.width * t.height);

However, as others have pointed out, using dynamic isn't really appropriate here. If you're using the query only within a method, an anonymous instance is good enough. If you want to use the result of the query outside the method, create a small struct or class or use Tuple<T1,T2>.

Upvotes: 6

casperOne
casperOne

Reputation: 74560

Dynamic is not your solution here. You simply have to calculate the area inside of the using statement.

Doing it outside of the using statement is a bad idea if you are trying to introduce deferred execution here; you will end up diposing of the StreamReader before you make the call to Sum.

In the end, your code should look like this:

int Area = 0;

using (var sr = new StreamReader("Data.txt", Encoding.UTF8)) 
{ 
    Area = sr.ReadToEnd().Split('\n').Select(r => 
    { 
        var split = r.Split(' '); 
        var len = split.Length; 
        return new { 
            w = len > 0 ? int.Parse(split[0].Trim()) : 0, 
            h = len > 1 ? int.Parse(split[1].Trim()) : 0  
        } as dynamic; 
    }).Sum(r => r.h * r.w);
} 

Doing it this way is better, you are not materializing the list and then looping through it again, you are pulling the data and summing it as needed.

Upvotes: 0

Konrad Rudolph
Konrad Rudolph

Reputation: 546073

The reason why you cannot simply assign is the following:

In the first case, you’re getting a List<SomeAnonymousType>. That type is completely unrelated to List<dynamic>, even if an implicit conversion from the anonymous type to dynamic exists. It’s a bit like implicitly converting a List<int> to a List<float>.

Upvotes: 1

Jackson Pope
Jackson Pope

Reputation: 14660

I'd get rid of the first line, then where the list is assigned declare it with var and move the last line inside the using statement:

int Area;
using (var sr = new StreamReader("Data.txt", Encoding.UTF8))
{
    var list = sr.ReadToEnd().Split('\n').Select(r =>
    {
        var split = r.Split(' ');
        var len = split.Length;
        return new {
            w = len > 0 ? int.Parse(split[0].Trim()) : 0,
            h = len > 1 ? int.Parse(split[1].Trim()) : 0 
        };
    }).ToList();
    Area = list.Sum(r => r.h * r.w);
}

Upvotes: 2

Albin Sunnanbo
Albin Sunnanbo

Reputation: 47068

It has to do with co/contravariance.

You can not cast List<T1> to List<T2>. Try with

List<object> objectList = new List<string>{"hello"};

and you get the compile error "Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>'"

It is to prevent the error

List<string> stringList = new List<string>{"hello"};
List<object> objectList = stringList; // compile error
objectList.Add(new Car()); // this would add a Car object to the stringList if the above line was allowed

Upvotes: 1

Related Questions