如何对 LINQ/lambdas/表达式树中的值类型进行编译时检查?

发布于 2024-11-01 16:25:25 字数 2058 浏览 1 评论 0原文

我使用以下代码以线程安全的方式设置 Control 属性:

private delegate void SetPropertyThreadSafeDelegate<TPropertyType>(Control @this, Expression<Func<TPropertyType>> property, TPropertyType value);

public static void SetPropertyThreadSafe<TPropertyType>(this Control @this, Expression<Func<TPropertyType>> property, TPropertyType value)
{
  var propertyInfo = (property.Body as MemberExpression ?? (property.Body as UnaryExpression).Operand as MemberExpression).Member as PropertyInfo;

  if (propertyInfo == null ||
      !propertyInfo.ReflectedType.IsAssignableFrom(@this.GetType()) ||
      @this.GetType().GetProperty(propertyInfo.Name, propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (propertyInfo.PropertyType.IsValueType &&
      !propertyInfo.PropertyType.IsAssignableFrom(typeof(TPropertyType)))
  {
    throw new ArgumentException(string.Format("Attempted to assign incompatible value type: expecting {0}, got {1}.", propertyInfo.PropertyType, typeof(TPropertyType)));
  }

  if (@this.InvokeRequired)
  {
    @this.Invoke(new SetPropertyThreadSafeDelegate<TPropertyType>(SetPropertyThreadSafe), new object[] { @this, property, value });
  }
  else
  {
    @this.GetType().InvokeMember(propertyInfo.Name, BindingFlags.SetProperty, null, @this, new object[] { value });
  }
}

它的调用方式如下:

downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 32);

这样做的原因是为了在编译时检查属性名称和类型分配。它非常适合标准对象,但对于值类型来说一切都有点梨形,因为编译器很乐意接受以下内容,这当然会在运行时爆炸:

downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 'c');
downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, long.MaxValue);

我已经修改了 SetPropertyThreadSafe 方法处理使用值类型的情况,如果使用不正确的类型作为参数,则抛出异常,但我真正寻找的是能够让此方法对 100% 执行编译时类型检查案例,即对象值类型。这是否可能,如果可以,我需要如何修改我的代码才能做到这一点?

I'm using the following code to set Control properties in a thread-safe manner:

private delegate void SetPropertyThreadSafeDelegate<TPropertyType>(Control @this, Expression<Func<TPropertyType>> property, TPropertyType value);

public static void SetPropertyThreadSafe<TPropertyType>(this Control @this, Expression<Func<TPropertyType>> property, TPropertyType value)
{
  var propertyInfo = (property.Body as MemberExpression ?? (property.Body as UnaryExpression).Operand as MemberExpression).Member as PropertyInfo;

  if (propertyInfo == null ||
      !propertyInfo.ReflectedType.IsAssignableFrom(@this.GetType()) ||
      @this.GetType().GetProperty(propertyInfo.Name, propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (propertyInfo.PropertyType.IsValueType &&
      !propertyInfo.PropertyType.IsAssignableFrom(typeof(TPropertyType)))
  {
    throw new ArgumentException(string.Format("Attempted to assign incompatible value type: expecting {0}, got {1}.", propertyInfo.PropertyType, typeof(TPropertyType)));
  }

  if (@this.InvokeRequired)
  {
    @this.Invoke(new SetPropertyThreadSafeDelegate<TPropertyType>(SetPropertyThreadSafe), new object[] { @this, property, value });
  }
  else
  {
    @this.GetType().InvokeMember(propertyInfo.Name, BindingFlags.SetProperty, null, @this, new object[] { value });
  }
}

It's called like so:

downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 32);

The reason for doing this is to get compile-time checking of property names and type assignments. It works perfectly for standard objects, but everything goes a bit pear-shaped with value types because the compiler is happy to accept the following, which of course bombs at runtime:

downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 'c');
downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, long.MaxValue);

I've already modified the SetPropertyThreadSafe method to handle the case when value types are used, and throw an exception if the incorrect type is used as an argument, but what I'm really loooking for is the ability to get this method to perform compile-time type checking for 100% of cases, i.e. objects and value types. Is this even possible and if so how would I need to modify my code to do this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

离笑几人歌 2024-11-08 16:25:25

将契约更改为:

public static void SetPropertyThreadSafe<TPropertyType, TValue>(
        this Control self,
        Expression<Func<TPropertyType>> property,
        TValue value)
        where TValue : TPropertyType

请注意,这样您就不再需要执行 IsAssignableFrom 检查,因为编译器将强制执行它。

您的示例编译的原因是编译器猜测了类型参数是什么。以下是编译器将这些调用转换为的内容:

progBar.SetPropertyThreadSafe<int>(() => progBar.Step, 'c');
progBar.SetPropertyThreadSafe<long>(() => progBar.Step, long.MaxValue);

请注意第一个调用是 int,这是因为 ProgressBar.Step 是 int,而“c”是 char,它隐式转换为 int。与下一个示例相同,int 隐式转换为 long,而第二个是 long,因此编译器猜测它是 long。

如果您希望像这样的继承和转换起作用,请不要让编译器猜测。您的两个解决方案是:

  1. 始终指定类型参数。在这种情况下,您会注意到第二个很长,并解决了问题。

当然,这不太理想,因为这样您基本上就对 Func 的类型进行了硬编码。您真正想做的是让编译器确定这两种类型并告诉您它们是否兼容。

  1. 为两者提供不同的类型,以便编译器可以为您计算出来。

注意:下面是我将使用的代码,它与您的完全不同:

    public static void SetPropertyThreadSafe<TControl>(this TControl self, Action<TControl> setter)
        where TControl : Control
    {
        if (self.InvokeRequired)
        {
            var invoker = (Action)(() => setter(self));
            self.Invoke(invoker);
        }
        else
        {
            setter(self);
        }
    }

    public static void Example()
    {
        var progBar = new ProgressBar();
        progBar.SetPropertyThreadSafe(p => p.Step = 3);
    }

Change the contract to:

public static void SetPropertyThreadSafe<TPropertyType, TValue>(
        this Control self,
        Expression<Func<TPropertyType>> property,
        TValue value)
        where TValue : TPropertyType

Note that with this you no longer need to do the IsAssignableFrom check since the compiler will enforce it.

The reasons your example compiled is because the compiler made a guess as to what the type parameter was. Here is what the compiler turns those calls into:

progBar.SetPropertyThreadSafe<int>(() => progBar.Step, 'c');
progBar.SetPropertyThreadSafe<long>(() => progBar.Step, long.MaxValue);

Notice how the first one is int, that's because ProgressBar.Step is an int and 'c' is a char which has an implicit conversion to int. Same with the next example, int has an implicit conversion to long, and the second one is long, so the compiler guesses that it is long.

If you want inheritance and conversions like those to work, don't make the compiler guess. Your two solutions are:

  1. Always specify the type perameter. In this case, you would have noticed that the second one is long, and fixed the problem.

Of course this is less than ideal, because then you are basically hard coding in the type of the Func. What you really want to do is let the compiler determine both types and tell you if they are compatible.

  1. Provide a different type for both so that the compiler can figure it out for you.

NOTE: Below is the code that I would use, which is entirely different from yours:

    public static void SetPropertyThreadSafe<TControl>(this TControl self, Action<TControl> setter)
        where TControl : Control
    {
        if (self.InvokeRequired)
        {
            var invoker = (Action)(() => setter(self));
            self.Invoke(invoker);
        }
        else
        {
            setter(self);
        }
    }

    public static void Example()
    {
        var progBar = new ProgressBar();
        progBar.SetPropertyThreadSafe(p => p.Step = 3);
    }
安人多梦 2024-11-08 16:25:25

你不能。 Linq 使用在运行时评估的表达式树。

我建议为您的查询创建单元测试。

You can't. Linq uses expression trees which are evaluated at runtime.

I would suggest creating unit tests for your queries.

初心未许 2024-11-08 16:25:25

您只需要对泛型和表达式进行一些小的更改:

public static void SetPropertyThreadSafe<TSource, TPropertyType>(this TSource source, Expression<Func<TSource, TPropertyType>> property, TPropertyType value)

然后您提供一个 lambda,如下所示:

var someObject = new /*Your Object*/

someObject.SetPropertyThreadSafe(x => x.SomeProperty, /* Your Value */);

您指定的值必须与 SomeProperty 的类型协变,并且会在编译时检查这一点。如果我误解了什么,请告诉我。如果您需要将其限制为 Control,只需将签名更改为

this Control source

where TSource : Control

You just need to make some minor changes to your generic and expression:

public static void SetPropertyThreadSafe<TSource, TPropertyType>(this TSource source, Expression<Func<TSource, TPropertyType>> property, TPropertyType value)

Then you supply a lambda like so:

var someObject = new /*Your Object*/

someObject.SetPropertyThreadSafe(x => x.SomeProperty, /* Your Value */);

The value you specify must be covariant to the type of SomeProperty, and this is checked at compile time. Let me know if I'm misunderstanding something. If you need to constrain it to Control, you simply change the signature to

this Control source

or

where TSource : Control
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文