可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I've noticed that the new ExpandoObject
implements IDictionary<string,object>
which has the requisite IEnumerable<KeyValuePair<string, object>>
and Add(string, object)
methods and so it should be possible to use the collection initialiser syntax to add properties to the expando object in the same way as you add items to a dictionary.
Dictionary<string,object> dict = new Dictionary<string,object>()
{
{ "Hello", "World" }
};
dynamic obj = new ExpandoObject()
{
{ "foo", "hello" },
{ "bar", 42 },
{ "baz", new object() }
};
int value = obj.bar;
But there doesn't seem to be a way of doing that. Error:
'System.Dynamic.ExpandoObject' does not contain a definition for 'Add'
I assume this doesn't work because the interface is implemented explicitly.
but is there any way of getting around that? This works fine,
IDictionary<string, object> exdict = new ExpandoObject() as IDictionary<string, object>();
exdict.Add("foo", "hello");
exdict.Add("bar", 42);
exdict.Add("baz", new object());
but the collection initializer syntax is much neater.
回答1:
I've had the need for a simple ExpandoObject initializer several times before and typically use the following two extension methods to accomplish something like initializer syntax:
public static KeyValuePair<string, object> WithValue(this string key, object value)
{
return new KeyValuePair<string, object>(key, value);
}
public static ExpandoObject Init(
this ExpandoObject expando, params KeyValuePair<string, object>[] values)
{
foreach(KeyValuePair<string, object> kvp in values)
{
((IDictionary<string, Object>)expando)[kvp.Key] = kvp.Value;
}
return expando;
}
Then you can write the following:
dynamic foo = new ExpandoObject().Init(
"A".WithValue(true),
"B".WithValue("Bar"));
In general I've found that having an extension method to build KeyValuePair<string, object>
instances from a string key comes in handy. You can obviously change the name to something like Is
so that you can write "Key".Is("Value")
if you need the syntax to be even more terse.
回答2:
The language specification (7.5.10.3 on Collection Initializers) is a bit vague on this point as far as I can tell. It says
For each specified element in order,
the collection initializer invokes an
Add method on the target object with
the expression list of the element
initializer as argument list, applying
normal overload resolution for each
invocation. Thus, the collection
object must contain an applicable Add
method for each element initializer.
Unfortunately the text doesn't go into details about what an applicable Add method is, but it seems that explicitly implemented interface methods don't fit the bill as they are essentially considered private (see 13.4.1):
It is not possible to access an
explicit interface member
implementation through its fully
qualified name in a method invocation,
property access, or indexer access. An
explicit interface member
implementation can only be accessed
through an interface instance, and is
in that case referenced simply by its
member name.
...
Explicit interface member
implementations have different
accessibility characteristics than
other members. Because explicit
interface member implementations are
never accessible through their fully
qualified name in a method invocation
or a property access, they are in a
sense private. However, since they can
be accessed through an interface
instance, they are in a sense also
public.
回答3:
The opensource framework Dynamitey has an alternative syntax for building ExpandoObject
instances inline.
dynamic obj = Builder.New<ExpandoObject>(
foo:"hello",
bar: 42 ,
baz: new object()
);
int value = obj.bar;
It also has a dictionary based dynamic prototype object Dynamitey.DynamicObjects.Dictionary
such that
dynamic obj = new Dynamitey.DynamicObjects.Dictionary()
{
{ "foo", "hello" },
{ "bar", 42 },
{ "baz", new object() }
};
int value = obj.bar;
works too.
回答4:
First of all, you are spot on. The IDictionary<string,object>
has been implemented explicitly.
You do not even need casting. This works:
IDictionary<string,object> exdict = new ExpandoObject()
Now the reason collection syntax does not work is because that is an implementation in the Dictionary<T,T>
constructor and not part of the interface hence it will not work for expando.
Wrong statement above. You are right, it uses add function:
static void Main(string[] args)
{
Dictionary<string,object> dictionary = new Dictionary<string, object>()
{
{"Ali", "Ostad"}
};
}
Gets compiled to
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 27 (0x1b)
.maxstack 3
.locals init ([0] class [mscorlib]System.Collections.Generic.Dictionary`2<string,object> dictionary,
[1] class [mscorlib]System.Collections.Generic.Dictionary`2<string,object> '<>g__initLocal0')
IL_0000: nop
IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,object>::.ctor()
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: ldstr "Ali"
IL_000d: ldstr "Ostad"
IL_0012: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,object>::Add(!0,
!1)
IL_0017: nop
IL_0018: ldloc.1
IL_0019: stloc.0
IL_001a: ret
} // end of method Program::Main
UPDATE
The main reason is Add
has been implemented as protected
(with no modifier which becomes protected
).
Since Add
is not visible on ExpandoObject
, it cannot be called as above.
回答5:
Using the idea from @samedave, and adding reflection, I got this:
void Main()
{
dynamic foo = new ExpandoObject().Init(new
{
A = true,
B = "Bar"
});
Console.WriteLine(foo.A);
Console.WriteLine(foo.B);
}
public static class ExtensionMethods
{
public static ExpandoObject Init(this ExpandoObject expando, dynamic obj)
{
var expandoDic = (IDictionary<string, object>)expando;
foreach (System.Reflection.PropertyInfo fi in obj.GetType().GetProperties())
{
expandoDic[fi.Name] = fi.GetValue(obj, null);
}
return expando;
}
}
But it would be nicer to be able to just do it like:
dynamic foo = new ExpandoObject
{
A = true,
B = "Bar"
});
Please vote for this feature in Visual Studio UserVoice.
Update:
Using Rick Strahl's Expando implementation you should be able to do something like this:
dynamic baz = new Expando(new
{
A = false,
B = "Bar2"
});
Console.WriteLine(baz.A);
Console.WriteLine(baz.B);
回答6:
It's a shame that adding dynamic properties (whose name is known only at run-time) to ExpandoObject
is not as easy as it should have been. All the casting to dictionary is plain ugly. Never mind you could always write a custom DynamicObject
that implements Add
which helps you with neat object initializer like syntax.
A rough example:
public sealed class Expando : DynamicObject, IDictionary<string, object>
{
readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return _properties.TryGetValue(binder.Name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (binder.Name == "Add")
{
var del = value as Delegate;
if (del != null && del.Method.ReturnType == typeof(void))
{
var parameters = del.Method.GetParameters();
if (parameters.Count() == 2 && parameters.First().ParameterType == typeof(string))
throw new RuntimeBinderException("Method signature cannot be 'void Add(string, ?)'");
}
}
_properties[binder.Name] = value;
return true;
}
object IDictionary<string, object>.this[string key]
{
get
{
return _properties[key];
}
set
{
_properties[key] = value;
}
}
int ICollection<KeyValuePair<string, object>>.Count
{
get { return _properties.Count; }
}
bool ICollection<KeyValuePair<string, object>>.IsReadOnly
{
get { return false; }
}
ICollection<string> IDictionary<string, object>.Keys
{
get { return _properties.Keys; }
}
ICollection<object> IDictionary<string, object>.Values
{
get { return _properties.Values; }
}
public void Add(string key, object value)
{
_properties.Add(key, value);
}
bool IDictionary<string, object>.ContainsKey(string key)
{
return _properties.ContainsKey(key);
}
bool IDictionary<string, object>.Remove(string key)
{
return _properties.Remove(key);
}
bool IDictionary<string, object>.TryGetValue(string key, out object value)
{
return _properties.TryGetValue(key, out value);
}
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{
((ICollection<KeyValuePair<string, object>>)_properties).Add(item);
}
void ICollection<KeyValuePair<string, object>>.Clear()
{
_properties.Clear();
}
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{
return ((ICollection<KeyValuePair<string, object>>)_properties).Contains(item);
}
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
((ICollection<KeyValuePair<string, object>>)_properties).CopyTo(array, arrayIndex);
}
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
{
return ((ICollection<KeyValuePair<string, object>>)_properties).Remove(item);
}
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
return _properties.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_properties).GetEnumerator();
}
}
And you could call like you wanted:
dynamic obj = new Expando()
{
{ "foo", "hello" },
{ "bar", 42 },
{ "baz", new object() }
};
int value = obj.bar;
The caveat with this approach is that you cannot add Add
"method" with the same signature as Dictionary.Add to your expando object since it is already a valid member of the Expando
class (which was required for the collection initializer syntax). The code throws an exception if you do
obj.Add = 1; // runs
obj.Add = new Action<string, object>(....); // throws, same signature
obj.Add = new Action<string, int>(....); // throws, same signature for expando class
obj.Add = new Action<string, object, object>(....); // runs, different signature
obj.Add = new Func<string, object, int>(....); // runs, different signature
If property names needn't be truly dynamic then another alternative is to have a ToDynamic
extension method so that you can initialize in-line.
public static dynamic ToDynamic(this object item)
{
var expando = new ExpandoObject() as IDictionary<string, object>;
foreach (var propertyInfo in item.GetType().GetProperties())
expando[propertyInfo.Name] = propertyInfo.GetValue(item, null);
return expando;
}
So you can call:
var obj = new { foo = "hello", bar = 42, baz = new object() }.ToDynamic();
int value = obj.bar;
There are a hundred ways you can design an API for this, another one such (mentioned in orad's answer) is:
dynamic obj = new Expando(new { foo = "hello", bar = 42, baz = new object() });
Will be trivial to implement.
Side note: there is always anonymous types if you know the property names statically and you dont want to add further after initialization.