tags:

views:

136

answers:

6

I'm working on an API which will read string values from a file and allow for type conversion. I also need it to provide a way of "mapping" given string values to a specific value of the desired type. That would look something like this:

int val = myObject.getValue<int>("FieldName", { { "", 0 }, { "INF", int.MaxValue } });

The field name is there so that it can be retrieved later if an error occurs. It's only important to this example because the function needs to take an unrelated first parameter.

However, I am having trouble coming up with an elegant and type-safe syntax to provide this functionality (the above, based on collection initializers, only works if I stick new FieldValueMappingCollection<int> { ... } in there).


So, the options I'm aware of are:

myObject.getValue<int>("FieldName", new FieldValueMappingCollection<int> { { "", 0 }, { "INF", int.MaxValue } });

or

myObject.getValue<int>("FieldName", "", 0, "INF", int.MaxValue);

where getValue has params object[] in its signature.

There's got to be a nicer way of doing this, right?

A: 

The usual way of doing this is:

myObject.getValue<int>("FieldName",
    Tuple.Create("", 0),
    Tuple.Create("INF", int.MaxValue));

using a function that takes a params array, and either .NET tuples or your own "tuple" class for these field entries.

I'm afraid there isn't really a more concise way in C# while retaining static typing. The collection initializer syntax won't let you get away with not specifying the type.

mquander
I should mention that I'm on .NET 3.5, but of course I could create my own Tuple class. There's still the ugliness and "wish there was a better way" feeling of having to specify the generic type again though.
jnylen
+1  A: 

How about the following class:

public class MyObject<T>
{

    public T GetValue(string fieldName, params MyObjectMap<T>[] mappings)
    {
       // Do whatever you need to do
    }

    public MyObjectMap<T> Map(string from, T to)
    {
        return MyObjectMap<T>.Map(from, to);
    }

}

public class MyObjectMap<T>
{

    private MyObjectMap(string from, T to) { }

    public static MyObjectMap<T> Map(string from, T to)
    {
        return new MyObjectMap<T>(from, to);
    }

}

You could use this like the following:

private void Foo()
{
    MyObject<int> myObject = new MyObject<int>();
    myObject.GetValue("FieldName",
        myObject.Map("", 0),
        myObject.Map("INF", int.MaxValue));
}

And it's entirely type safe.

GenericTypeTea
Not type-safe enough: here you'd have to specify `FieldValueMap.Map<int>()` each time. Seems like I am asking for too much though.
jnylen
Why not just have additional methods? MapInt, MapString, etc? Or am I not understanding the question?
GenericTypeTea
No, I think you got it... I am just being extremely picky about syntax choices. I don't especially like the fact that to retain type safety, I'd have to specify `<int>` for each value pair (or `FieldValueMap.MapInt`). Looks like I won't really be able to get away with it though.
jnylen
Maybe a `Map<T>` method.
TTT
@jnylen - Updated my answer, should work :).
GenericTypeTea
Nice trick, I didn't think of that. I wish I could use it, but `myObject` is a file reader that will need to do type conversions to several different types (`int`, `DateTime`, etc.) Again it looks like I am asking C# for too much.
jnylen
So if you could do `myObject.GetValue<int>(...);` and only specify `int` once, that'd be acceptable?
GenericTypeTea
That's what I was hoping for. It doesn't seem possible though, so I just used a Map<int> method.
jnylen
Accepted because I asked a nitpicky impossible question and you put a lot of effort into it, including some cool tricks. Thanks.
jnylen
+2  A: 

A nicer way of doing this would be to use a Dictionary<String, Int32> or whatever. Set it up once in your class, and then simply pass the dictionary in. Since you're making an API, this is a much cleaner and expressive way to declare your intent rather than using Tuples. A dictionary is built for these types of operations, and your users should be able to easily understand the intent of the method. Tuples are much less understandable, especially when you're dealing with a simply Key-Value mapping.

In addition, it makes for much nicer initialization if you want to initialize on the fly: http://msdn.microsoft.com/en-us/library/bb531208.aspx

drharris
This is essentially what I'm doing with the `FieldValueMappingCollection<T>`.
jnylen
Correct, except a Dictionary is even more generic (are you certain you'll never need to map an integer to a string?), shorter to type, and easier to understand the intent of what's going on.
drharris
Yes, I'm absolutely sure that the "from" type will never be anything except a string. To me, that's a good argument for not using a dictionary because there you have to specify that your keys are strings every time.
jnylen
+1  A: 

How about passing a delegate:

myObject.GetValue<int>("FieldName", value =>
{
    if (string.IsNullOrEmpty(value))
        return 0;
    if (value == "INF")
        return int.MaxValue;
    throw new InvalidInputException();
});

Advantages:

  • This supports a much wider range of possibilities than just a one-to-one mapping of values.
  • If it gets complex, you can make it a method of its own instead of needing to inline it here.
  • It is completely type-safe.
  • The compiler enforces that you need to return a value or throw an exception. You can declare your own InvalidInputException to communicate to GetValue<>() that you couldn’t process the input.

Disadvantages:

  • Verbose...
  • If GetValue<>() only calls the delegate when it can’t process the input itself, then you can’t use this to override values that GetValue<>() already considers valid, e.g. you can’t make it reject −1.
  • If GetValue<>() calls the delegate before it does its own processing, there is a performance penalty in throwing the exception every time.
Timwi
You're starting to hint at larger considerations of our design. My thought in writing this API was to provide very basic operations like type conversion and mapping string values to desired output values, but there will be a section of the code which will do extensive error checking that will sometimes include multiple fields at once. Do you think the error checking should maybe be done all at once (for example, in per-field delegates) instead?
jnylen
I really don’t have enough information to answer that question. You certainly can’t validate multiple fields from a delegate that has access to only one field. It is also unclear whether you want to check the input strings for validity (e.g. whether a string constitutes a valid date in some format), or the consistency of the output data (e.g. whether a particular date is earlier or later than another). It seems to me that the former is string-specific and should go into the delegate, while the latter is application-specific and should be done in a separate step.
Timwi
There will be both kinds of checking, and probably lots of it. I was going for an approach that would provide an interface for doing simple, formulaic operations like mapping values and converting types, but the more complicated operations will come later. So I think the delegate approach is overkill for this part of the application.
jnylen
A: 

I would not send the information as parameters. I would use a fluent syntax instead, something like this:

object.With("", 0)
      .With("INF", int.MaxValue)
      .Get<int>("FieldName");

You can change the method names to make it look as you want to. Other option could be:

object.GetValueUsing("", 0)
      .AndUsing("INF", int.MaxValue)
      .Of<int>("FieldName");

Or:

object.WithParameter("", 0)
      .AndParameter("INF", int.MaxValue)
      .GetValue<int>("FieldName");

If you don't know how to create a fluent interface, just google for "c# creating fluent interface" and you will tons of samples.

Diego Jancic
I have only done something like this in Javascript, where it works very naturally. I will read up on it in C#, but first: it seems like you lose type safety here, right?
jnylen
Well, since you are passing parameters in text you will loose many things. You can still make many overloads of the methods to set parameters. Or make a generic one (like With<string>("something", "text")).Or you can even make the most used parameters defined. ie. WithInf(int), which calls to With(string, object).You can really customize the API. You might want to check the API from http://code.google.com/p/nunitex/
Diego Jancic
A: 

I would consider doing something like the example below.

Pros:

  • simple and compact syntax using anonymous types

Cons:

  • Potentially doing a lot of reflection
  • keys are restricted to valid identifiers, i.e. special characters are not allowed

The GetValue method takes 4 parameters:

  1. the field name, which I assume is used to look up the appropriate value from the file
  2. A default value in case the string value is ""
  3. An object, assumed to be an anonymous type, used as a value map. The public properties act as the keys of the map, and the values of those properties act as the corresponding values.
  4. A parse delegate that is used when the correct value cannot be identified through the value map

(Seems like I need a line here after the list to avoid messing up the code formatting)

    public T GetValue<T>(string fieldName, T @default, object valueMap, Converter<string, T> parse)
    {
        T value;
        string literalString = null; // read string from file

        if (string.IsNullOrEmpty(literalString))
            return @default;
        else
        {
            var map = ToDictionary<T>(valueMap);
            // attempt to look up the corresponding value associated with literalString
            if (map.TryGetValue(literalString, out value))
                return value;
            else
                // literalString does not match any value in the value map,
                // so parse it using the provided delegate
                return parse(literalString);
        }
    }

    public static Dictionary<string, T> ToDictionary<T>(object valueMap)
    {
        // Use reflection to read public properties and add them as keys to a
        // dictionary along with their corresponding value
        // The generic parameter enforces that all properties
        // must be of the specified type,
        // otherwise the code here can throw an exception or ignore the property
        throw new NotImplementedException();
    }


        // usage

        int field1 = myObject.GetValue("Field1", 0, new { one = 1, two = 2, TheAnswer = 42, min = int.MinValue, max = int.MaxValue, INF = int.MaxValue }, int.Parse);

        double field2 = myObject.GetValue("Field2", 0.0, new { PI = Math.PI, min = double.MinValue, max = double.MaxValue }, double.Parse);
Dr. Wily's Apprentice