How to use a ref parameter in a thread if "Error CS1628: Cannot use in ref or out parameter inside an anonymous method, lambda or query expression"?

Viewed 204

I am trying to write a tweening class, and in order to do so I have a static Tweener.TweenTo method. In it, we start a thread in order to not hold up external operations. The method looks like this:

public static void TweenTo<T>(ref ITweenable<T> obj, T target, double ms)
{
    new System.Threading.Thread(() => {
        System.Threading.Thread.Sleep(5000)
        obj.DoStuff(5,5) //throws exception because obj is a ref parameter
    }).Start()
}

I understand that using the ref parameter in the lambda means that it may be a dangling reference, but I need to be able to use it in case the user tries to pass a value type or struct to the method. I have tried using parameterized thread start, but that coerces things to an object, which I cannot use (unboxing etc.). I have also tried using a wrapper class that holds a pointer to it, but this runs in to complications later down the line.

I would like a way of using this ref parameter in the thread, and ideally preserving its lifetime inside it.

Any help is appreciated :D

Edit: Olivier's answer close to what I need, but MyClass would be a struct in some cases, and it will duplicate whenever it can. This means that it would lose the reference and give values from the wrong instance.

Edit 2: example struct

public struct MyStruct : ITweenable<T> {
    int x;
    int y;
    public MyStruct(int X) {
        this.x=X;
    }
    public void DoStuff(int newX, int newY) {
         this.x=newX;
         this.y=newY;
    }
}

public interface ITweenable<T> {
    void DoStuff(int newX, int newY);
}

Edit 3: I haven't tested this so when I have I will give it as an answer - have tested now, doesn't work:

public static void TweenTo<T>(ITweenable<T> obj, T target, double ms)
{
Func<ITweenable<T>> getTween = ()=>{return obj;}
    new System.Threading.Thread(() => {
        System.Threading.Thread.Sleep(5000)
        getTween().DoStuff(5,5);
    }).Start()
}
3 Answers

Using an intermediate local var

Without considering the design and the reason for having the parameter by ref, nor any thread concurrency and interlocking management, just use an intermediate local var:

public static void TweenTo<T>(ref ITweenable<T> obj, T target, double ms)
{
  var instance = obj;
  new System.Threading.Thread(instance.DoStuff).Start();
}

Test

public interface ITweenable<T>
{
  void DoStuff();
}

public class MyClass : ITweenable<int>
{
  public void DoStuff()
  {
    Console.WriteLine("It works!");
    Console.WriteLine("Press any key to exit the side thread.");
    Console.ReadKey();
  }
}

static private void Test()
{
  var instance = (ITweenable<int>)new MyClass();
  TweenTo(ref instance, 10, 20);
  Console.WriteLine("Main thread ended.");
}

Output

Main thread ended.
It works!
Press any key to exit the side thread.

Remark for struct and code added in the question

It works the same, for the code and the case provided:

public interface ITweenable<T>
{
  T X { get; }
  T Y { get; }
  void DoStuff(T newX, T newY);
}

public struct MyStruct : ITweenable<int>
{
  public int X { get; private set; }
  public int Y { get; private set; }
  public void DoStuff(int newX, int newY)
  {
    Thread.Sleep(2000);
    X = newX;
    Y = newY;
    Console.WriteLine("It works!");
  }
}

public static void TweenTo<T>(ref ITweenable<T> obj, T target, double ms)
{
  var instance = obj;
  new System.Threading.Thread(() => instance.DoStuff((T)(object)10, (T)(object)10)).Start();
  Console.WriteLine("Exiting TweenTo.");
}

Test

static private void Test()
{
  var instance = (ITweenable<int>)new MyStruct();
  Console.WriteLine("X is " + instance.X);
  TweenTo(ref instance, 10, 20);
  Console.WriteLine("Main thread ended.");
  Console.WriteLine("Wait for the 'It Works' and press any key to continue main thread.");
  Console.ReadKey();
  Console.WriteLine("X is now " + instance.X);
  Console.ReadKey();
}

Output

X is 0
Exiting TweenTo.
Main thread ended.
Wait for the 'It Works' and press any key to continue main thread.
It works!
X is now 10

I'd suggest passing an Action<ITweenable<T>> to enable the assignment. Like this:

public static void TweenTo<T>(ITweenable<T> obj, Action<ITweenable<T>> update, T target, double ms)
{
    new System.Threading.Thread(() =>
    {
        obj.DoStuff();
        update(new Tweenable<T>());
    }).Start()
}

Values by reference cannot be used across threads or in any deferred execution because .NET cannot enforce its memory management.

  1. The struct in the ref lives on the stack of the calling function and can only be used when inside the scope of that function call.

  2. The struct is a field in an other class and the reference is a pointer pointing to somewhere in the middle of the objects memory. The Garbage Collector cannot track these pointers and they are not allowed to live longer than the function call that created this reference. This function call has the original pointer to the object on its stack and with that the Garbage Collector can track the object.

So, in short, objects that need to be tweenable cannot be structs. The only solution is to make the class that contains this struct tweenable. Or if the tweenables are contained in an array implement a helper like this:

public class TweenableArray<T> : ITweenable<T>
{
    private readonly ITweenable<T>[] _data;

    public TweenableArray(ITweenable<T>[] data)
    {
        _data = data
    }

    void DoStuff(int newX, int newY)
    {
        for (var i = 0; i < _data.Length; i++)
            _data[i].DoStuff(newX, newY)
    }
}

Provided that newX and newY can be used for the whole array.

Related