Including a Method class inside an Expression

2019-05-23 08:49发布

问题:

Given the following

class MyClass
{
    private Expression<Func<SomeEntity, int>> _orderBy;

    public MyClass(Expression<Func<SomeEntity, int>> orderBy)
    {
        _orderBy = orderBy;
    }

    public List<SomeEntity> Fetch()
    {
        return DbContext.Set<SomeEntity>().OrderBy(_orderBy).ToList();
    }
}

// A function that creates an orderBy expression
Expression<Func<SomeEntity, int>> SomeFunction()
{
    // r is a local variable for the sake of simplicity,
    // actually it is a global static variable.
    Random r = new Random();
    int seed = r.nextInt();

    // s.SomeProperty XOR seed, a simple random sorting method
    return s => (s.SomeProperty & ~seed) | (~s.SomeProperty & seed);
}

And the executing code

var myClass = new MyClass( SomeFunction() );

List<SomeEntity> someList = myClass.Fetch();
Thread.Sleep(100000);
List<SomeEntity> anotherList = myClass.Fetch();

Each time Fetch() is called, a randomly sorted list must be returned. The problem is, seed = r.nextInt() won't be called each time I call Fetch(). How do I ensure that a new seed is generated every time Fetch() is called?

回答1:

One possible solution is pass in a factory function that generates the order by functions instead of the function itself, now it generates a new Expression<Func<SomeEntity, int>> every time you call Fetch() each with a new seed.

class MyClass
{
    Func<Expression<Func<SomeEntity, int>>> _orderByFactory;

    public MyClass(Func<Expression<Func<SomeEntity, int>>> orderByFactory)
    {
        _orderByFactory = orderByFactory;
    }


    public List<SomeEntity> Fetch()
    {
        var orderBy = _orderByFactory();

        return DbContext.Set<SomeEntity>().OrderBy(orderBy).ToList();
    }
}

//...

// Moved r out of the function as it needs to be outside for it to work.
static Random r = new Random();

static Expression<Func<SomeEntity, int>> SomeFunction()
{
    int seed = r.Next();

    // s.SomeProperty XOR seed, a simple random sorting method
    return s => (s.SomeProperty & ~seed) | (~s.SomeProperty & seed);
}

//...

var myClass = new MyClass(SomeFunction); //<--Notice the removed parenthesis
List<SomeEntity> someList = myClass.Fetch();
Thread.Sleep(100000);
List<SomeEntity> anotherList = myClass.Fetch();


回答2:

Simply move the call to r.Next() into your delegate. The following code (a bit dumbed down from your code since I didn't want to have to actually order anything) returns different numbers each time. Sorry for the poor variable names...

FYI, the variable r gets elevated out from the method SomeFunction() onto the heap, and will stick around and be used any time the Expression> gets called. See This question and the link in the answer for more information on closures.

EDIT: I've moved the random number generation to its own class that seems to work...

internal class MyClass {
    private Expression<Func<string, int>> _aProperty;

    public MyClass(Expression<Func<string, int>> aProperty) {
        _aProperty = aProperty;
    }

    public int Fetch() {
        var something = _aProperty.Compile();
        return something("doesn't matter");
    }
}

public class DoubleUp {
    private Random r = new Random();
    private int currentValue;
    public int A { get { return currentValue; } }
    public int B { get { int tmp = currentValue; currentValue = r.Next(); return tmp; } }

    public DoubleUp() {
        currentValue = r.Next();
    }
}

internal class Program {

    private static Expression<Func<string, int>> SomeFunction() {
        DoubleUp d = new DoubleUp();
        return s => (d.A - d.B);
    }

    private static void Main(string[] args) {
        var myClass = new MyClass(SomeFunction());
        Console.WriteLine(myClass.Fetch());
        Console.WriteLine(myClass.Fetch());
        Console.WriteLine(myClass.Fetch());
        Console.ReadLine();
    }
}


回答3:

The problem is you only call SomeFunction once to set the seed. And you can't reset the seed within the Expression because an expression cannot contain a function body.

An alternative would be to use 4 bytes of a new Guid as a pseudo-random number generator:

// A function that creates an orderBy expression
Expression<Func<int, int>> SomeFunction()
{
    // Take 4 bytes from a new Guid and turn it into a 32-bit integer
    return s=> BitConverter.ToInt32(Guid.NewGuid().ToByteArray(),0);
}


回答4:

Make the expression a Func that you call to get a new Orderby each time Fetch is called:

class MyClass
{
    private Func<Expression<Func<SomeEntity, int>>> _orderByFactory;

    public MyClass(Func<Expression<Func<SomeEntity, int>>> orderByFactory)
    {
        _orderByFactory = orderByFactory;
    }

    public List<SomeEntity> Fetch()
    {
       var orderBy = _orderByFactory();
       return DbContext.Set<SomeEntity>().OrderBy(orderBy).ToList();
    }
}

And the call becomes

var myClass = new MyClass( () => SomeFunction() );
List<SomeEntity> someList = myClass.Fetch();


回答5:

Given your problem statement:

Each time Fetch() is called, a randomly sorted list must be returned.

It seems to me you're over-thinking things. Why on earth wouldn't you just do something simple like:

class MyClass
{
  private static Random rng = new Random() ;

  public List<SomeEntity> Fetch()
  {
    return DbContext.Set<SomeEntity>()
                    .Cast<SomeEntity>()
                    .OrderBy( x => rng.Next() )
                    .ToList()
                    ;
  }

}