实现INotifyPropertyChanged - 是否存在更好的方法?

P.K*_*P.K 621 .net c# inotifypropertychanged winforms

微软应该已经实现了一些有趣的东西INotifyPropertyChanged,就像在自动属性中一样,只需指定{get; set; notify;} 我认为这样做很有意义.或者有任何并发​​症吗?

我们自己可以在我们的属性中实现类似'notify'的内容.是否有一个优雅的解决方案,INotifyPropertyChanged在您的班级实施或唯一的方法是通过提高PropertyChanged每个属性中的事件.

如果没有,我们可以写一些东西来自动生成一段代码来引发PropertyChanged 事件吗?

Mar*_*ell 611

不使用像postharp这样的东西,我使用的最小版本使用如下:

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    protected bool SetField<T>(ref T field, T value, string propertyName)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}
Run Code Online (Sandbox Code Playgroud)

每个属性都是这样的:

    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
Run Code Online (Sandbox Code Playgroud)

这不是很大; 如果你愿意,它也可以用作基类.在bool从回SetField告诉你,如果它是一个空操作,如果你想申请其他逻辑.


或者更容易使用C#5:

protected bool SetField<T>(ref T field, T value,
    [CallerMemberName] string propertyName = null)
{...}
Run Code Online (Sandbox Code Playgroud)

可以像这样调用:

set { SetField(ref name, value); }
Run Code Online (Sandbox Code Playgroud)

编译器将"Name"自动添加.


C#6.0使实现更容易:

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Run Code Online (Sandbox Code Playgroud)

......现在使用C#7:

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
   => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));


private string name;
public string Name
{
    get => name;
    set => SetField(ref name, value);
}
Run Code Online (Sandbox Code Playgroud)

  • @Marc - 是的,它可能会降低性能...但是我真的很喜欢在编译时检查它并且通过"重命名"命令正确重构的事实 (13认同)
  • @Thomas - lambda一切都很好,但它为实际上非常简单的东西增加了很多开销.一个方便的技巧,但我不确定它总是实用的. (7认同)
  • 好戏法Marc!我建议改进使用lambda表达式而不是属性名称,请参阅我的回答 (4认同)
  • @Gusdor幸运的是,使用C#5没有必要妥协 - 你可以通过(如Pedro77注释)`[CallerMemberName]来获得最佳效果 (4认同)
  • @Gusdor的语言和框架是分开的; 您可以使用C#5编译器,目标.NET 4,只需**自己添加缺少的属性** - 它将正常工作.它必须具有正确的名称并且位于正确的命名空间中.它不需要在特定的程序集中. (4认同)
  • DevXpress Xpo就是这样做的. (3认同)
  • 这很酷。但是,在 MVVM 中,根据我的经验,模式通常是 ViewModel 包装模型。所以没有后台。相反,我们需要将 model.Name 设置为一个值。我们不能将 model.Name 作为 ref 传入。 (3认同)
  • 那么:private void RaisePropertyChanged([CallerMemberName] string caller =""){if(PropertyChanged!= null){PropertyChanged(this,new PropertyChangedEventArgs(caller)); }} (2认同)
  • @Gusdor [CallerMemberName]可用于.Net 4.0,使用Microsoft.Bcl nuget包!http://www.nuget.org/packages/Microsoft.Bcl/ (2认同)
  • @FandiSusanto` [CallerMemberName]`是C#5编译器识别的参数属性,并自动注入调用方法/成员的名称(作为常量字符串,因此实习等); 对于`null`传播,`?.`语法是C#6; 如果左边的对象是非"空",则调用该方法; 如果对象为null,则不会.如果隐含了返回值,如果对象为"null",则它将假定值为"null". (2认同)

Dan*_*tle 196

从.Net 4.5开始,最终有一种简单的方法可以做到这一点.

.Net 4.5引入了新的呼叫者信息属性.

private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
     // make sure only to call this if the value actually changes

     var handler = PropertyChanged;
     if (handler != null) {
        handler(this, new PropertyChangedEventArgs(caller));
     }
}
Run Code Online (Sandbox Code Playgroud)

为函数添加比较器可能是个好主意.

EqualityComparer<T>.Default.Equals
Run Code Online (Sandbox Code Playgroud)

这里这里有更多的例子

另请参阅来电者信息(C#和Visual Basic)

  • 辉煌!但为什么它是通用的? (12认同)
  • @J.Lennon .net 4.5仍然与它有关,毕竟属性来自某个地方http://msdn.microsoft.com/en-au/library/system.runtime.compilerservices.callermembernameattribute.aspx (5认同)
  • 它是由C#5.0引入的.它与.net 4.5无关,但这是一个很好的解决方案! (3认同)

Tho*_*que 162

我真的很喜欢Marc的解决方案,但我认为可以略微改进以避免使用"魔术字符串"(不支持重构).不是将属性名称用作字符串,而是很容易使它成为lambda表达式:

private string name;
public string Name
{
    get { return name; }
    set { SetField(ref name, value, () => Name); }
}
Run Code Online (Sandbox Code Playgroud)

只需将以下方法添加到Marc的代码中,它就可以实现:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
    if (selectorExpression == null)
        throw new ArgumentNullException("selectorExpression");
    MemberExpression body = selectorExpression.Body as MemberExpression;
    if (body == null)
        throw new ArgumentException("The body must be a member expression");
    OnPropertyChanged(body.Member.Name);
}

protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(selectorExpression);
    return true;
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这是受此博客帖子 更新URL的启发

  • 至少有一个框架使用这种方法,[ReactiveUI](http://www.reactiveui.net). (6认同)
  • 我相信你的整个OnPropertyChanged <T>已经过时了C#6的运营商名称,让这个怪物变得更加时髦. (6认同)
  • @Traubenfuchs,实际上,C#5的CallerMemberName属性使它更简单,因为你根本不需要传递任何东西...... (5认同)
  • @BrunoBrant 您确定性能受到影响吗?根据博客文章,反射发生在编译时而不是运行时(即静态反射)。 (2认同)

Tom*_*der 117

还有Fody,它有一个PropertyChanged加载项,可以让你写这个:

[ImplementPropertyChanged]
public class Person 
{        
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

...并在编译时注入属性更改通知.

  • 我认为这正是OP在他们问我们"我们自己可以在我们的房产中实现'通知'之类的东西时所寻找的.在你的班级中实施INotifyPropertyChanged是否有优雅的解决方案" (7认同)
  • 这是唯一优雅的解决方案,它确实可以完美地运行,如@CADbloke所说.而且我对织布工也持怀疑态度,但我检查/重新检查了背后的IL代码并且它是完美的,它很简单,只需要你所需要的而且没有别的.它还挂钩并调用你在基类中指定的任何方法名称,无论NotifyOnProp ...,OnNotify ......都无关紧要,因此适用于你可能拥有的任何基类并实现INotify. . (3认同)
  • 您可以轻松地仔细检查编织器正在做什么,查看构建输出窗口,它列出了它编织的所有 PropertyChanged 内容。使用 VScolorOutput 扩展和正则表达式模式 `"Fody/.*?:",LogCustom2,True` 会以“Custom 2”颜色突出显示它。我把它变成了亮粉色,这样很容易找到。一切都只需 Fody,这是完成任何需要大量重复输入的事情的最简洁方式。 (2认同)
  • 我同意@Damien。从版本 [3.4.0](https://www.nuget.org/packages/PropertyChanged.Fody) 开始,此属性已弃用。按照文档的建议,使用“AddINotifyPropertyChangedInterfaceAttribute”对我有用。 (2认同)

Pei*_*jen 64

我认为人们应该多关注一下性能,当有很多对象需要绑定时(考虑一个有10,000多行的网格)或者对象的值经常变化(实时监控应用程序),它确实会对UI产生影响.

我在这里和其他地方进行了各种实现并进行了比较,检查了INotifyPropertyChanged实现的性能比较.


这是对结果的看法 实施与运行时间

  • -1:没有性能开销:CallerMemberName在编译时更改为文字值.只需尝试反编译您的应用. (13认同)
  • 如果您的 UI 中有超过 10,000 个网格,那么您可能应该结合多种方法来处理性能,例如分页,每页仅显示 10、50、100、250 次点击... (2认同)

TiM*_*och 38

我在博客http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/中介绍了一个Bindable类 .Bindable使用字典作为属性包.为子类添加必要的重载以使用ref参数管理自己的支持字段非常容易.

  • 没有魔法字符串
  • 没有反思
  • 可以改进以抑制默认字典查找

代码:

public class Bindable : INotifyPropertyChanged {
    private Dictionary<string, object> _properties = new Dictionary<string, object>();

    /// <summary>
    /// Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    protected T Get<T>([CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T)value;
        return default(T);
    }

    /// <summary>
    /// Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    /// <remarks>Use this overload when implicitly naming the property</remarks>
    protected void Set<T>(T value, [CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        if (Equals(value, Get<T>(name)))
            return;
        _properties[name] = value;
        OnPropertyChanged(name);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它可以像这样使用:

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很好的解决方案,但唯一的缺点是有一个小的性能打击涉及拳击/拆箱. (2认同)

Mar*_*ris 15

我实际上还没有机会尝试这个,但下次我正在设置一个对INotifyPropertyChanged有很大要求的项目我打算写一个Postsharp属性,它将在编译时注入代码.就像是:

[NotifiesChange]
public string FirstName { get; set; }
Run Code Online (Sandbox Code Playgroud)

会变成:

private string _firstName;

public string FirstName
{
   get { return _firstname; }
   set
   {
      if (_firstname != value)
      {
          _firstname = value;
          OnPropertyChanged("FirstName")
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

我不确定这是否会在实践中发挥作用,我需要坐下来尝试一下,但我不明白为什么不这样做.对于需要触发多个OnPropertyChanged的情况,我可能需要让它接受一些参数(例如,如果我在上面的类中有一个FullName属性)

目前我在Resharper中使用了一个自定义模板,但即便如此,我已经厌倦了我所有的属性这么长时间.


嗯,一个快速谷歌搜索(这是我写这之前,我应该这样做)显示,至少有一个人已经做过这样的事情在这里.不完全是我的想法,但足够接近表明理论是好的.

  • 一个名为Fody的免费工具似乎做同样的事情,作为通用的编译时代码注入器.它可以在Nuget中下载,PropertyChanged和PropertyChanging插件包也可以下载. (6认同)

Joh*_*ohn 12

现在已经是 2022 年了。现在官方有了解决方案。

使用Microsoft MVVM Toolkit 中的MVVM 源生成器

[ObservableProperty]
private string? name;
Run Code Online (Sandbox Code Playgroud)

将生成:

private string? name;

public string? Name
{
    get => name;
    set
    {
        if (!EqualityComparer<string?>.Default.Equals(name, value))
        {
            OnNameChanging(value);
            OnPropertyChanging();
            name = value;
            OnNameChanged(value);
            OnPropertyChanged();
        }
    }
}

// Property changing / changed listener
partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);

protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
    PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}

protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Run Code Online (Sandbox Code Playgroud)

它支持 .NET 标准 2.0 和 .NET >= 5.0。

  • @WSC 语言的设计方式应使其不需要为_obvious_ 和_natural_ 设计模式生成代码和部分类。 (2认同)

tes*_*der 11

是的,当然存在更好的方法.这里是:

基于这篇有用的文章,一步一步的教程缩小了.

  • 创建新项目
  • 将castle core package安装到项目中

安装包Castle.Core

  • 仅安装mvvm light库

安装包MvvmLightLibs

  • 在项目中添加两个类:

NotifierInterceptor

public class NotifierInterceptor : IInterceptor
    {
        private PropertyChangedEventHandler handler;
        public static Dictionary<String, PropertyChangedEventArgs> _cache =
          new Dictionary<string, PropertyChangedEventArgs>();

        public void Intercept(IInvocation invocation)
        {
            switch (invocation.Method.Name)
            {
                case "add_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                case "remove_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                default:
                    if (invocation.Method.Name.StartsWith("set_"))
                    {
                        invocation.Proceed();
                        if (handler != null)
                        {
                            var arg = retrievePropertyChangedArg(invocation.Method.Name);
                            handler(invocation.Proxy, arg);
                        }
                    }
                    else invocation.Proceed();
                    break;
            }
        }

        private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
        {
            PropertyChangedEventArgs arg = null;
            _cache.TryGetValue(methodName, out arg);
            if (arg == null)
            {
                arg = new PropertyChangedEventArgs(methodName.Substring(4));
                _cache.Add(methodName, arg);
            }
            return arg;
        }
    }
Run Code Online (Sandbox Code Playgroud)

ProxyCreator

public class ProxyCreator
{
    public static T MakeINotifyPropertyChanged<T>() where T : class, new()
    {
        var proxyGen = new ProxyGenerator();
        var proxy = proxyGen.CreateClassProxy(
          typeof(T),
          new[] { typeof(INotifyPropertyChanged) },
          ProxyGenerationOptions.Default,
          new NotifierInterceptor()
          );
        return proxy as T;
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 创建视图模型,例如:

-

 public class MainViewModel
    {
        public virtual string MainTextBox { get; set; }

        public RelayCommand TestActionCommand
        {
            get { return new RelayCommand(TestAction); }
        }

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
Run Code Online (Sandbox Code Playgroud)
  • 将绑定放入xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
    
    Run Code Online (Sandbox Code Playgroud)
  • 将代码行放在代码隐藏文件MainWindow.xaml.cs中,如下所示:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • 请享用.

在此输入图像描述

注意!!!所有有界属性都应使用关键字virtual进行修饰,因为它们由castle proxy用于覆盖.


Hok*_*ike 7

类似AOP的方法是将INotifyPropertyChanged内容注入已经实例化的对象中.您可以使用Castle DynamicProxy之类的功能执行此操作.这是一篇解释该技术的文章:

将INotifyPropertyChanged添加到现有对象


Kel*_*lyn 5

让我介绍一下我自己的方法,称为Yappi。它属于运行时代理|派生类生成器,向现有对象或类型添加新功能,如 Caste Project 的动态代理。

它允许在基类中实现 INotifyPropertyChanged 一次,然后按以下样式声明派生类,仍然支持新属性的 INotifyPropertyChanged:

public class Animal:Concept
{
    protected Animal(){}
    public virtual string Name { get; set; }
    public virtual int Age { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

派生类或代理构造的复杂性可以隐藏在以下行后面:

var animal = Concept.Create<Animal>.New();
Run Code Online (Sandbox Code Playgroud)

所有 INotifyPropertyChanged 实现工作都可以这样完成:

public class Concept:INotifyPropertyChanged
{
    //Hide constructor
    protected Concept(){}

    public static class Create<TConcept> where TConcept:Concept
    {
        //Construct derived Type calling PropertyProxy.ConstructType
        public static readonly Type Type = PropertyProxy.ConstructType<TConcept, Implementation<TConcept>>(new Type[0], true);
        //Create constructing delegate calling Constructor.Compile
        public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
    {
        var caller = PropertyChanged;
        if(caller!=null)
        {
            caller(this, eventArgs);
        }
    }

    //define implementation
    public class Implementation<TConcept> : DefaultImplementation<TConcept> where TConcept:Concept
    {
        public override Func<TBaseType, TResult> OverrideGetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            return PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
        }
        /// <summary>
        /// Overriding property setter implementation.
        /// </summary>
        /// <typeparam name="TBaseType">Base type for implementation. TBaseType must be TConcept, and inherits all its constraints. Also TBaseType is TDeclaringType.</typeparam>
        /// <typeparam name="TDeclaringType">Type, declaring property.</typeparam>
        /// <typeparam name="TConstructedType">Constructed type. TConstructedType is TDeclaringType and TBaseType.</typeparam>
        /// <typeparam name="TResult">Type of property.</typeparam>
        /// <param name="property">PropertyInfo of property.</param>
        /// <returns>Delegate, corresponding to property setter implementation.</returns>
        public override Action<TBaseType, TResult> OverrideSetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            //This code called once for each declared property on derived type's initialization.
            //EventArgs instance is shared between all events for each concrete property.
            var eventArgs = new PropertyChangedEventArgs(property.Name);
            //get delegates for base calls.
            Action<TBaseType, TResult> setter = PropertyImplementation<TBaseType, TDeclaringType>.GetSetter<TResult>(property.Name);
            Func<TBaseType, TResult> getter = PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);

            var comparer = EqualityComparer<TResult>.Default;

            return (pthis, value) =>
            {//This code executes each time property setter is called.
                if (comparer.Equals(value, getter(pthis))) return;
                //base. call
                setter(pthis, value);
                //Directly accessing Concept's protected method.
                pthis.OnPropertyChanged(eventArgs);
            };
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它对于重构来说是完全安全的,类型构造后不使用反射并且足够快。


小智 5

请看这里:http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx

它是用德语编写的,但您可以下载ViewModelBase.cs.cs-File中的所有注释都是用英文写的.

使用此ViewModelBase-Class,可以实现类似于众所周知的依赖项属性的可绑定属性:

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}
Run Code Online (Sandbox Code Playgroud)

  • 链接坏了。 (2认同)

Ofi*_*fir 5

所有这些答案都非常好。

我的解决方案是使用代码片段来完成这项工作。

这使用了对 PropertyChanged 事件的最简单的调用。

保存此代码片段并像使用“fullprop”代码片段一样使用它。

该位置可以在 Visual Studio 的“工具\代码片段管理器...”菜单中找到。

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>inotifypropfull</Title>
            <Shortcut>inotifypropfull</Shortcut>
            <HelpUrl>http://ofirzeitoun.wordpress.com/</HelpUrl>
            <Description>Code snippet for property and backing field with notification</Description>
            <Author>Ofir Zeitoun</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>Property type</ToolTip>
                    <Default>int</Default>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>Property name</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>The variable backing this property</ToolTip>
                    <Default>myVar</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
                <![CDATA[private $type$ $field$;

    public $type$ $property$
    {
        get { return $field$;}
        set { 
            $field$ = value;
            var temp = PropertyChanged;
            if (temp != null)
            {
                temp(this, new PropertyChangedEventArgs("$property$"));
            }
        }
    }
    $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>
Run Code Online (Sandbox Code Playgroud)

您可以根据需要修改调用(使用上述解决方案)


Dan*_*Dan 5

虽然显然有很多方法可以做到这一点,但除了 AOP 魔术答案之外,没有一个答案似乎是直接从视图模型设置模型的属性,而不需要引用本地字段。

问题是您无法引用属性。但是,您可以使用操作来设置该属性。

protected bool TrySetProperty<T>(Action<T> property, T newValue, T oldValue, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
    {
        return false;
    }

    property(newValue);
    RaisePropertyChanged(propertyName);
    return true;
}
Run Code Online (Sandbox Code Playgroud)

这可以像下面的代码摘录一样使用。

public int Prop {
    get => model.Prop;
    set => TrySetProperty(x => model.Prop = x, value, model.Prop);
}
Run Code Online (Sandbox Code Playgroud)

查看此BitBucket 存储库,了解该方法的完整实现以及实现相同结果的几种不同方法,包括使用 LINQ 的方法和使用反射的方法。请注意,这些方法的性能较慢。