Nick
Nick

Reputation: 745

Passing property as parameter

I am creating a merit function calculator, which for the uninitiated takes a selection of properties, and calculates a value based on how close those properties are to some idealized values (the merit function). This then enables the user to find an item that most closely matches their requirements.

This is the code I'd like to use:

public class MeritFunctionLine
{
    public Func<CalculationOutput, double> property { get; set; }
    public double value { get; set; }
    public ComparisonTypes ComparisonType { get; set; }
}

public class MeritFunction
{
    public List<MeritFunctionLine> Lines { get; set; }
    public double Calculate(CalculationOutput values)
    {
        double m = 0;
        foreach (var item in Lines)
        {
            m += Math.Abs(values.property - item.value);
        }
        return m;
    }
}

public class CalculationOutput
{
    public double property1 { get; set; }
    public double property2 { get; set; }
    public double property3 { get; set; }
    public double property4 { get; set; }
}

Obviously this doesn't compile as values doesn't contain a member called property, but here is an explanation of what I want to do:

  1. Create a new MeritFunction
  2. Add an arbitrary number of MeritFunctionLines to MeritFunction.Lines
  3. The MeritFunctionLine.property should specify what property of CalculationOutput should be compared in MeritFunction.Calculate

i.e.

MeritFunction mf = new MeritFunction();
mf.Lines.Add(new MeritFunctionLine() { property = x => x.Property1, value = 90, comparisonType = ComparisonTypes.GreaterThan });
mf.Lines.Add(new MeritFunctionLine() { property = x => x.Property3, value = 50, comparisonType = ComparisonTypes.Equals });

CalculationOutput c1 = new CalculationOutput() { property1 = 1, property2 = 20, property3 = 150, property4 = 500 };
CalculationOutput c2 = new CalculationOutput() { property1 = 15, property2 = 32, property3 = 15, property4 = 45 };

double value1 = mf.Calculate(c1);
double value2 = mf.Calculate(c2);

I am not asking how to pass a property as a parameter into a function, which is prohibited by C#.

Upvotes: 11

Views: 8493

Answers (2)

Medo42
Medo42

Reputation: 3821

You almost have the right solution already - the only missing piece is how you use the MeritFunctionLine.property property to get the desired value from the CalculationOutput.

In your foreach loop, simply replace the calculation line with

m += Math.Abs(item.property(values) - item.value);

Edit:

Adding Genericity

To address Obsidian Phoenix's comment, you can use this with different classes by making both MeritFunction and MeritFunctionLine generic, so:

public class MeritFunctionLine<TCalcOutput>
{
    public Func<TCalcOutput, double> property { get; set; }
    public double value { get; set; }
    public ComparisonTypes ComparisonType { get; set; }
}

public class MeritFunction<TCalcOutput>
{
    public List<MeritFunctionLine<TCalcOutput>> Lines { get; set; }
    public double Calculate(TCalcOutput values)
    {
        double m = 0;
        foreach (var item in Lines)
        {
            m += Math.Abs(item.property(values) - item.value);
        }
        return m;
    }
}

The rewritten usage example would be

MeritFunction<CalculationOutput> mf = new MeritFunction<CalculationOutput>();
mf.Lines.Add(new MeritFunctionLine<CalculationOutput>() { property = x => x.Property1, value = 90, comparisonType = ComparisonTypes.GreaterThan });
mf.Lines.Add(new MeritFunctionLine<CalculationOutput>() { property = x => x.Property3, value = 50, comparisonType = ComparisonTypes.Equals });

CalculationOutput c1 = new CalculationOutput() { property1 = 1, property2 = 20, property3 = 150, property4 = 500 };
CalculationOutput c2 = new CalculationOutput() { property1 = 15, property2 = 32, property3 = 15, property4 = 45 };

double value1 = mf.Calculate(c1);
double value2 = mf.Calculate(c2);

Some extra convenience

If you have many MeritFunctionLines to add, the syntax above can be a bit tedious. So as a bonus, let's change MeritFunction so that it can be initialized with the list initialization syntax. To do that, we need to make it IEnumerable and give it an Add function:

public class MeritFunction<TCalcOutput> : IEnumerable<MeritFunctionLine<TCalcOutput>>
{
    public List<MeritFunctionLine<TCalcOutput>> Lines { get; set; }

    public MeritFunction()
    {
        Lines = new List<MeritFunctionLine<TCalcOutput>>();
    }

    public void Add(Func<TCalcOutput, double> property, ComparisonTypes ComparisonType, double value)
    {
        Lines.Add(new MeritFunctionLine<CalculationOutput>
        {
            property = property,
            value = value,
            comparisonType = ComparisonType
        });
    }

    public double Calculate(TCalcOutput values)
    {
        double m = 0;
        foreach (var item in Lines)
        {
            m += Math.Abs(item.property(values) - item.value);
        }
        return m;
    }

    public IEnumerator<MeritFunctionLine<TCalcOutput>> GetEnumerator()
    {
        return List.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

Note that the Add method receives the parameters in a different order - you'll understand why when you look at the usage. Quite a bit of extra code, but now creating our MeritFunction is a bit nicer:

MeritFunction<CalculationOutput> mf = new MeritFunction<CalculationOutput>
{
    { x => x.Property1, ComparisonTypes.GreaterThan, 90 },
    { x => x.Property3, ComparisonTypes.Equals,      50 }
};

Note, all code untested. Use at own risk :)

Upvotes: 10

Obsidian Phoenix
Obsidian Phoenix

Reputation: 4155

It is possible, but it's not exactly pretty. You can make use of Expression<Func<double>> to pass in the property, then use reflection to pull the value back out.

NB: I have not coded this to accomodate for error scenarios, you may want to add additional checks.

class Program
{
    static void Main(string[] args)
    {
        MeritFunction<CalculationOutput> mf = new MeritFunction<CalculationOutput>();

        //Create an instance of the object for reference.
        var obj = new CalculationOutput();

        //Use Lambda to set the Property Expression on the Line, pointing at the Property we are interested in.
        mf.Lines.Add(new MeritFunctionLine() { PropertyExpression = () => obj.property1, value = 90, ComparisonType = ComparisonTypes.GreaterThan });
        mf.Lines.Add(new MeritFunctionLine() { PropertyExpression = () => obj.property3, value = 50, ComparisonType = ComparisonTypes.Equals });

        CalculationOutput c1 = new CalculationOutput() { property1 = 1, property2 = 20, property3 = 150, property4 = 500 };
        CalculationOutput c2 = new CalculationOutput() { property1 = 15, property2 = 32, property3 = 15, property4 = 45 };

        double value1 = mf.Calculate(c1);
        double value2 = mf.Calculate(c2);

        Console.WriteLine(value1);
        Console.WriteLine(value2);
    }
}

public class MeritFunctionLine
{
    //Capture an expression representing the property we want.
    public Expression<Func<double>> PropertyExpression { get; set; }

    public double value { get; set; }
    public ComparisonTypes ComparisonType { get; set; }
}

public class MeritFunction<T>
{
    public List<MeritFunctionLine> Lines { get; set; }

    public MeritFunction()
    {
        Lines = new List<MeritFunctionLine>();
    }

    public double Calculate(T values)
    {
        double m = 0;
        foreach (var item in Lines)
        {
            //Get the Value before calculating.
            double value = ExtractPropertyValue(item, values);

            m += Math.Abs(value - item.value);
        }
        return m;
    }

    /// <summary>
    /// Take the Provided Expression representing the property, and use it to extract the property value from the object we're interested in.
    /// </summary>
    private double ExtractPropertyValue(MeritFunctionLine line, T values)
    {
        var expression = line.PropertyExpression.Body as MemberExpression;
        var prop = expression.Member as PropertyInfo;

        double value = (double)prop.GetValue(values);

        return value;
    }
}

public class CalculationOutput
{
    public double property1 { get; set; }
    public double property2 { get; set; }
    public double property3 { get; set; }
    public double property4 { get; set; }
}

public enum ComparisonTypes
{
    GreaterThan,
    Equals
}

The one gotcha of this method, is that you need to create an instance of the object whilst building up the Lines property, otherwise you can't actually access the property through the lambda.

If you only need this for a single class, then this is likely overkill, but it will work with essentially any class.

Upvotes: 4

Related Questions