Constraining a generic method to struct and string

A

Anders Borum

Hi!

While implementing a property manager (that supports key / value pairs), I
was wondering how to constrain T to a struct or string type. Basically, I
guess what I'm looking for is the common denominator for structs and strings
and while looking through the SDK I only noticed the IEquatable<T>
interface.

So I implemented the class with methods such as the following, but I'm aware
that this is not the right approach.

// Initial implementation
public void Add<T>(string key, T value) where T : IEquatable<T>
{
this.AddProperty(key, value);
}

// Proposed implementation
public void Add<T>(string key, T value) where T : IEquatable<T>
{
if (! (value is ValueType) || ! (value is string)) { throw new
InvalidOperationException("message"); }

this.AddProperty(key, value);
}
 
J

Jeroen Mostert

Anders said:
While implementing a property manager (that supports key / value pairs),
I was wondering how to constrain T to a struct or string type.

Though it's not the same as implementing a single method, you can do it with
overloads:

public void Foo<T>(T value) where T : struct { ... }
public void Foo(string value) { ... }

However, I would be very surprised if you actually needed such a construct,
so it's worth taking a closer look.
Basically, I guess what I'm looking for is the common denominator for
structs and strings and while looking through the SDK I only noticed the
IEquatable<T> interface.
There is no common denominator for structs and strings, other than Object,
and it's unclear what exactly you're after. In what circumstances would it
be meaningful to restrict types to structs or strings? Keep in mind that a
struct is really just a sealed class with value type semantics, while a
string is an immutable reference type. They're not at all comparable.

What exactly do you require of keys that makes you think you need to
restrict them to strings and structs?
So I implemented the class with methods such as the following, but I'm
aware that this is not the right approach.

// Initial implementation
public void Add<T>(string key, T value) where T : IEquatable<T>
{
this.AddProperty(key, value);
}

// Proposed implementation
public void Add<T>(string key, T value) where T : IEquatable<T>
{
if (! (value is ValueType) || ! (value is string)) { throw new
InvalidOperationException("message"); }
What's "message"? :)
this.AddProperty(key, value);
}
Again, what are you trying to do here? Why do you think you need to use
value types and strings? If you want the ability to copy an object,
constrain the type to ICloneable and use .Clone(). If you need to compare
them for equality, then IEquatable<> is indeed a good choice. It seems like
you're imposing an arbitrary restriction that doesn't gain you anything.
 
M

Marc Gravell

 And even if  
they did, there's nothing about your description that suggests that you  
really care about the members of the IEquatable<T> interface anyway.

Absolutely; but I know where you're coming from - most times people
ask about "struct and strings" it is to detect changes in values (for
things such as INotifyPropertyChanged). Actually, in many cases I've
found that the IEquatable<T> method just gets in the way - especially
if generics are involved (constraints accumulate, which can mean you
have piles of constraints). I generally use
EqualityComparer<T>.Default instead of an explicit IEquatable<T>
constraint - this works the same (slightly different IL of course),
but doesn't need the constraint, and also works (via Equals) for
legacy types that don't implement IEquatable<T>.

Marc
 
A

Anders Borum

Hi Jeroen,
Though it's not the same as implementing a single method, you can do it
with overloads:

public void Foo<T>(T value) where T : struct { ... }
public void Foo(string value) { ... }

That's a good idea and I'm quite surprised I didn't think of that. It's
definately a lot better because of the compiler checks instead of runtime
exception throwing so I'll stick with that.
There is no common denominator for structs and strings, other than Object,
and it's unclear what exactly you're after. In what circumstances would it
be meaningful to restrict types to structs or strings? Keep in mind that a
struct is really just a sealed class with value type semantics, while a
string is an immutable reference type. They're not at all comparable.

The PropertyManager class I'm writing is exposed from business objects. The
backing store for the manager is XML and thus the properties needs to be
serializable / deserializable in order to quickly parse each value to the
the concrete type (be it a single key / value pair or key / list<value>
pair). For deserialization, I'm doing a switch statement in a worker method
to obtain a lambda method that recognizes the actual type (stored with the
key/value pair in the XML backing store).

For types (or structs) that is unknown, a fallback implementation relies in
reflection to invoke a Parse(string) method on the type (as is found in the
value types in the BCL). This approach was chosen because of performance
requirements and because of the expected general usage pattern by other
developers (most properties are stored as string or numerics, but I decided
to support custom structs as well).

Please note that this method could be refactored for greater performance
where the actual parsed value is returned instead of a lambda. This is
especially valuable when parsing lists of property values where the lambda
can be reused.

private static Func<string, object> CreateParser(string typeName)
{
switch (typeName)
{
case "System.String": { return (p) => p; }
case "System.Guid": { return (p) => new Guid(p); }
case "System.Bool": { return (p) => bool.Parse(p); }
case "System.Byte": { return (p) => byte.Parse(p); }
case "System.Char": { return (p) => char.Parse(p); }
case "System.Decimal": { return (p) => decimal.Parse(p); }
case "System.Double": { return (p) => double.Parse(p); }
case "System.Single": { return (p) => float.Parse(p); }
case "System.Int16": { return (p) => short.Parse(p); }
case "System.Int32": { return (p) => int.Parse(p); }
case "System.Int63": { return (p) => long.Parse(p); }
case "System.SByte": { return (p) => sbyte.Parse(p); }
case "System.UInt16": { return (p) => ushort.Parse(p); }
case "System.UInt32": { return (p) => uint.Parse(p); }
case "System.UInt64": { return (p) => ulong.Parse(p); }
default:
{
Type type = Type.GetType(typeName);
MethodInfo info = type.GetMethod("Parse", new Type[] {
typeof(string) });

if (info == null) { throw new
InvalidOperationException(string.Format("Unable to find the Parse(string
value) method for type: {0}.", type.FullName)); }

return (p) => info.Invoke(null, new object[] { p });
}
}
}
What exactly do you require of keys that makes you think you need to
restrict them to strings and structs?

They're immutable. It's important that the value never changes once the
manager has received it.
Again, what are you trying to do here? Why do you think you need to use
value types and strings? If you want the ability to copy an object,
constrain the type to ICloneable and use .Clone(). If you need to compare
them for equality, then IEquatable<> is indeed a good choice. It seems
like you're imposing an arbitrary restriction that doesn't gain you
anything.

Yes, the arbitrary constraint was just implemented as a work-in-progres
constraint.

What I take from the above is that I should provide two overloads;

1. where T : struct
2. where T : ICloneable

Regardless, for unknown types I'd still require the class / struct to
provide a Parse(string) method that can be called via reflection when
deserializing the values. The PropertyManager is ment for storing small
configuration values, not large serialized object hierarchies. Although the
latter can be achieved using XmlSerialization and storing the string, it's
not what was originally intended.

Aside from that, let me state that the PropertyManager works very well, and
the only thing I really needed was a decision on constraints.
 
M

Marc Gravell

Regardless, for unknown types I'd still require the class / struct to
provide a Parse(string) method that can be called via reflection when
deserializing the values.

Can I recommend XmlConvert, i.e.

XmlConvert.To[...] (deserialize), or XmlConvert.ToString (serialize)

This will use suitable xml formats, and (IIRC) invariant culture.
As a secondary to XmlConvert, TypeDescriptor is probably a next best
thing - i.e.

TypeDescriptor.GetConverter(typeof(T)).Convert[To|From]InvariantString

Also - watch for a typo (Int63) - that would break things pretty
quickly...

Of course, another approach would be to use XmlSerializer or
DataContractSerializer, which will cope with more scenarios than I
care to have to list.

Marc
 
A

Anders Borum

Hi Marc

Sorry for the delay (I was busy writing APIs). Thanks for pointing out the
typo!

I decided to go with the XmlConvert because of the locale independence as I
couldn't find any reason to reproduce the implementation in that class just
to save a few method calls. I would have liked a generic
XmlConvert.ToString(object) signature, but I guess the implementation I came
up with is quite good in terms of maintenance and performance (the latter is
great).
Of course, another approach would be to use XmlSerializer or
DataContractSerializer, which will cope with more scenarios than I
care to have to list.

Again, the PropertyManager was intended as a storage for generic
configuration values / list of values. Not entire serialized graphs
(although it's definately supported by means of using a string configuration
value).

Here's the "CmsXmlConverter" class I ended up implementing - it may be of
use to others. As can be read from above, I'd have liked an
XmlConvert.ToString(object) method, but decided to skip the reflection
(emit) approch and wrote a switched implementation instead (which works very
well, I might add).

If you've got additional comments, I'm always very interested - and thanks
for the information so far!

public static string GetString(string typeName, object value)
{
if (value == null) { return null; }

switch (typeName)
{
case "System.String": { return (string) value; }
case "System.Guid": { return XmlConvert.ToString((Guid) value); }
case "System.DateTime": { return XmlConvert.ToString((DateTime) value,
XmlDateTimeSerializationMode.Utc); }
case "System.DateTimeOffset": { return
XmlConvert.ToString((DateTimeOffset) value); }
case "System.TimeSpan": { return XmlConvert.ToString((TimeSpan)
value); }
case "System.Bool": { return XmlConvert.ToString((bool) value); }
case "System.Byte": { return XmlConvert.ToString((byte) value); }
case "System.Char": { return XmlConvert.ToString((char) value); }
case "System.Decimal": { return XmlConvert.ToString((decimal) value); }
case "System.Double": { return XmlConvert.ToString((double) value); }
case "System.Single": { return XmlConvert.ToString((float) value); }
case "System.Int16": { return XmlConvert.ToString((short) value); }
case "System.Int32": { return XmlConvert.ToString((int) value); }
case "System.Int64": { return XmlConvert.ToString((long) value); }
case "System.SByte": { return XmlConvert.ToString((sbyte) value); }
case "System.UInt16": { return XmlConvert.ToString((ushort) value); }
case "System.UInt32": { return XmlConvert.ToString((uint) value); }
case "System.UInt64": { return XmlConvert.ToString((ulong) value); }
default: { return value.ToString(); }
}
}

public static object GetObject(string typeName, string value)
{
if (string.IsNullOrEmpty(value)) { return null; }

return GetParser(typeName)(value);
}

public static Func<string, object> GetParser(Type type)
{
return GetParser(type.FullName);
}

public static Func<string, object> GetParser(string typeName)
{
switch (typeName)
{
case "System.String": { return (p) => p; }
case "System.Guid": { return (p) => XmlConvert.ToGuid(p); }
case "System.DateTime": { return (p) => XmlConvert.ToDateTime(p,
XmlDateTimeSerializationMode.Utc); }
case "System.DateTimeOffset": { return (p) =>
XmlConvert.ToDateTimeOffset(p); }
case "System.TimeSpan": { return (p) => XmlConvert.ToTimeSpan(p); }
case "System.Bool": { return (p) => XmlConvert.ToBoolean(p); }
case "System.Byte": { return (p) => XmlConvert.ToByte(p); }
case "System.Char": { return (p) => XmlConvert.ToChar(p); }
case "System.Decimal": { return (p) => XmlConvert.ToDecimal(p); }
case "System.Double": { return (p) => XmlConvert.ToDouble(p); }
case "System.Single": { return (p) => XmlConvert.ToSingle(p); }
case "System.Int16": { return (p) => XmlConvert.ToInt16(p); }
case "System.Int32": { return (p) => XmlConvert.ToInt32(p); }
case "System.Int64": { return (p) => XmlConvert.ToInt64(p); }
case "System.SByte": { return (p) => XmlConvert.ToSByte(p); }
case "System.UInt16": { return (p) => XmlConvert.ToUInt16(p); }
case "System.UInt32": { return (p) => XmlConvert.ToUInt32(p); }
case "System.UInt64": { return (p) => Convert.ToUInt64(p); }
default:
{
Type type = Type.GetType(typeName);
MethodInfo info = type.GetMethod("Parse", new Type[] {
typeof(string) });

if (info == null) { throw new
InvalidOperationException(string.Format("Unable to find the Parse(string
value) method for type: {0}.", type.FullName)); }

return (p) => info.Invoke(null, new object[] { p });
}
}
}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top