使用坐标 c# 从 bindinglist<T> 获取和设置值

Get and set value from bindinglist<T> with coordinates c#

当类型不同时,我无法获取和设置带有坐标的绑定列表中项目的值。

例如,假设我有三个 类:

public class Client{
    public string Name{ get; set; }
}

public class Debt{
    public string AccountType{ get; set; }
    public int DebtValue { get; set; }
}

public class Accounts{
    public string Owner{ get; set; }
    public int AccountNumber { get; set; }
    public bool IsChekingAccount { get; set; }
}

然后是三个绑定列表(假设它们已填充):

public BindingList<Client> listOne;
public BindingList<Debt> listTwo;
public BindingList<Accounts> listThree;

我正在尝试创建一个扩展方法,该方法 returns 一个具有所请求值的对象,或者设置已提供的值。

public static Object GetValueByCoordinates(this IBindingList list, int x, int y) { /*some magic*/ }

public static Object SetValueByCoordinates(this IBindingList list, int x, int y, Object value) { /*some other magic*/ }

因此,例如,我需要能够设置 listThree 中的项目 (2,3) 的值,以及 listTwo 中的值 (1,1):

listThree.SetValueByCoordinates(2,3,false);
listThree.SetValueByCoordinates(1,1,"My self");

或者从listOne和listTwo中获取值(1,1)和(2,2):

string result = listOne.GetValueByCoordinates(1,1).ToString();
intresult = Convert.ToInt32(listOne.GetValueByCoordinates(1,1));

您将如何实现这种行为?我正在考虑使用反射,但我对此知之甚少。

请注意必须以这种方式调用方法,因此必须避免使用类似的方法

public static Object GetValueByCoordinates<T>(this BindingList<T> list, int x, int y) { /*some magic*/ }

我们将不胜感激。

如前所述,我非常怀疑您寻求帮助的方法可能是解决您要解决的更广泛问题的最佳或最合适的方法。可以做到(而且难度不大),但生成的代码难以维护、容易出错且可读性差(导致前两个问题)。

就是说,有很多不同的方法可以实现您所要求的特定行为。即使这不是解决您当前问题的最佳方法,了解这些基本技术对于解决其他类型的问题也很有用。考虑到这一点,这里有两种最明显的方法可以解决您的问题。


手动配置从索引到 getter 和 setter 的映射:

恕我直言,这是最可取的方式。不是因为它优雅或易于扩展,而是因为它 不是 这两点。要求代码维护者显式创建数据结构元素以支持您要处理的每种类型和 属性 将阻碍此技术在其他相关问题甚至当前问题中的扩散。它甚至可以鼓励人们花更多的时间思考更广泛的问题,以便找到更好的策略。

这种方法确实具有性能合理的优势。因为代码是在编译时生成的,所以唯一真正的开销是对值类型进行的装箱。有一些转换,但对于引用类型,开销实际上应该是无法测量的,甚至装箱开销也可能不会显示在配置文件中,具体取决于此代码的使用强度。

这个特定的解决方案如下所示:

static class ManualIndexedProperty
{
    public static void SetValueByCoordinates(this IBindingList list, int x, int y, object value)
    {
        object o = list[x];

        _typeToSetter[o.GetType()][y](o, value);
    }

    public static object GetValueByCoordinates(this IBindingList list, int x, int y)
    {
        object o = list[x];

        return _typeToGetter[o.GetType()][y](o);
    }

    private static readonly Dictionary<Type, Func<object, object>[]> _typeToGetter =
        new Dictionary<Type, Func<object, object>[]>()
        {
            {
                typeof(Client),
                new Func<object, object>[]
                {
                    o => ((Client)o).Name
                }
            },

            {
                typeof(Debt),
                new Func<object, object>[]
                {
                    o => ((Debt)o).AccountType,
                    o => ((Debt)o).DebtValue,
                }
            },

            {
                typeof(Accounts),
                new Func<object, object>[]
                {
                    o => ((Accounts)o).Owner,
                    o => ((Accounts)o).AccountNumber,
                    o => ((Accounts)o).IsChekingAccount,
                }
            },
        };

    private static readonly Dictionary<Type, Action<object, object>[]> _typeToSetter =
        new Dictionary<Type, Action<object, object>[]>()
        {
            {
                typeof(Client),
                new Action<object, object>[]
                {
                    (o1, o2) => ((Client)o1).Name = (string)o2
                }
            },

            {
                typeof(Debt),
                new Action<object, object>[]
                {
                    (o1, o2) => ((Debt)o1).AccountType = (string)o2,
                    (o1, o2) => ((Debt)o1).DebtValue = (int)o2,
                }
            },

            {
                typeof(Accounts),
                new Action<object, object>[]
                {
                    (o1, o2) => ((Accounts)o1).Owner = (string)o2,
                    (o1, o2) => ((Accounts)o1).AccountNumber = (int)o2,
                    (o1, o2) => ((Accounts)o1).IsChekingAccount = (bool)o2,
                }
            },
        };
}

声明了两个字典,每个字典用于设置和获取 属性 值。字典将元素对象的类型映射到委托实例数组以执行实际工作。每个委托实例都引用一个匿名方法,该方法已被手动编码以执行必要的操作。

这种方法的一个主要优点是,对于每种类型,什么索引对应什么 属性 是明确和明显的。

如果您要处理大量类型的 and/or 属性,那么设置这种方法将是乏味且耗时的。但恕我直言,这是一件好事。正如我上面提到的,希望这种方法的痛苦可以帮助说服某人完全放弃通过索引访问属性的想法。 :)

如果这种乏味是不可接受的,但你仍然坚持索引-属性-访问方法,那么你实际上可以使用反射作为替代方法......


使用反射访问属性:

这种技术更加动态。一旦实现,它无需修改即可适用于任何类型对象,并且不需要额外的工作来支持新类型。

一个主要的缺点是为了产生一致的、可预测的结果,它按名称对属性进行排序。这确保了 C# 编译器 and/or CLR 中的更改不会破坏代码,但这意味着您不能在不更新按索引访问这些属性的代码的情况下从类型中添加或删除属性。

在我的演示使用代码中(详见下文),我通过声明为 属性 名称提供 int 值的 enum 类型来解决此维护问题。如果代码实际上引用具有文字索引值的属性,这将是帮助减少维护开销的好方法。

但是,您的场景可能涉及按索引动态访问 属性 值,例如在序列化场景或类似场景中。在这种情况下,如果向类型添加或删除属性,您还需要添加一些可以重新映射或以其他方式处理索引值更改的内容。

坦率地说,无论哪种方式,类型索引更改的问题都是我强烈建议首先反对对属性进行索引访问的一个重要原因。但同样,如果你坚持......

static class ReflectionIndexedProperty
{
    public static void SetValueByCoordinates(this IBindingList list, int x, int y, object value)
    {
        object o = list[x];

        GetProperty(o, y).SetValue(o, value);
    }

    public static object GetValueByCoordinates(this IBindingList list, int x, int y)
    {
        object o = list[x];

        return GetProperty(o, y).GetValue(o);
    }

    private static PropertyInfo GetProperty(object o, int index)
    {
        Type type = o.GetType();
        PropertyInfo[] properties;

        if (!_typeToProperty.TryGetValue(type, out properties))
        {
            properties = type.GetProperties();
            Array.Sort(properties, (p1, p2) => string.Compare(p1.Name, p2.Name, StringComparison.OrdinalIgnoreCase));
            _typeToProperty[type] = properties;
        }

        return properties[index];
    }

    private static readonly Dictionary<Type, PropertyInfo[]> _typeToProperty = new Dictionary<Type, PropertyInfo[]>();
}

在此版本中,代码检索给定类型的 PropertyInfo 对象数组,按名称对该数组进行排序,为给定索引检索适当的 PropertyInfo 对象,然后使用它PropertyInfo 对象以根据需要设置或获取 属性 值。

反射会产生显着的 运行 时间性能开销。这个特定的实现通过缓存 PropertyInfo 对象的排序数组来减轻 一些 的开销。这样,它们只需要创建一次,即第一次代码必须处理给定类型的对象时。


演示代码:

正如我提到的,为了更容易比较这两种方法,而不必去每个方法调用并手动更改用于调用的整数文字,我创建了一些简单的 enum 类型来表示属性 索引。我还写了一些代码来初始化一些可以测试的列表。

注意: 需要指出的一件非常重要的事情是,在您的问题中,您对属性的索引方式不是很一致。在我的代码示例中,我选择坚持使用基于 0 的索引(与 C# 数组和其他集合中使用的自然索引一致)。您当然可以使用不同的基数(例如,基于 1 的索引),但是您需要确保在整个代码中完全一致(包括在实际索引数组时从传入的索引中减去 1)。

我的演示代码如下所示:

class Program
{
    static void Main(string[] args)
    {
        BindingList<Client> listOne = new BindingList<Client>()
        {
            new Client { Name = "ClientName1" },
            new Client { Name = "ClientName2" },
            new Client { Name = "ClientName3" },
        };

        BindingList<Debt> listTwo = new BindingList<Debt>()
        {
            new Debt { AccountType = "AccountType1", DebtValue = 29 },
            new Debt { AccountType = "AccountType2", DebtValue = 31 },
            new Debt { AccountType = "AccountType3", DebtValue = 37 },
        };

        BindingList<Accounts> listThree = new BindingList<Accounts>()
        {
            new Accounts { Owner = "Owner1", AccountNumber = 17, IsChekingAccount = false },
            new Accounts { Owner = "Owner2", AccountNumber = 19, IsChekingAccount = true },
            new Accounts { Owner = "Owner3", AccountNumber = 23, IsChekingAccount = true },
        };

        LogList(listThree);

        listThree.SetValueByCoordinates(2, (int)AccountsProperty.IsChekingAccount, false);
        listThree.SetValueByCoordinates(1, (int)AccountsProperty.Owner, "My self");

        LogList(listThree);

        string result1 = (string)listOne.GetValueByCoordinates(0, (int)ClientProperty.Name);
        int result2 = (int)listTwo.GetValueByCoordinates(1, (int)DebtProperty.DebtValue);

        LogList(listOne);
        LogList(listTwo);

        Console.WriteLine("result1: " + result1);
        Console.WriteLine("result2: " + result2);
    }

    static void LogList<T>(BindingList<T> list)
    {
        foreach (T t in list)
        {
            Console.WriteLine(t);
        }

        Console.WriteLine();
    }
}

请注意,我使用简单的转换从 object 转换为特定类型,包括设置 属性 值和获取它们。这是比例如更好的方法。调用 ToString()Convert.ToInt32();您确切地知道类型应该是什么,它要么是该类型的实际实例(对于引用类型),要么是盒装实例(对于值类型),无论哪种方式,强制转换都能满足您的需要。

我还向您的示例 类 添加了 ToString() 覆盖,以便更容易查看输出:

public class Client
{
    public string Name { get; set; }

    public override string ToString()
    {
        return "{" + Name + "}";
    }
}

public class Debt
{
    public string AccountType { get; set; }
    public int DebtValue { get; set; }

    public override string ToString()
    {
        return "{" + AccountType + ", " + DebtValue + "}";
    }
}

public class Accounts
{
    public string Owner { get; set; }
    public int AccountNumber { get; set; }
    public bool IsChekingAccount { get; set; }

    public override string ToString()
    {
        return "{" + Owner + ", " + AccountNumber + ", " + IsChekingAccount + "}";
    }
}

最后,这里是使用的 enum 声明:

手动索引:

enum ClientProperty
{
    Name = 0
}

enum DebtProperty
{
    AccountType = 0,
    DebtValue = 1
}

enum AccountsProperty
{
    Owner = 0,
    AccountNumber = 1,
    IsChekingAccount = 2,
}

Reflection/sorted 按姓名:

enum ClientProperty
{
    Name = 0
}

enum DebtProperty
{
    AccountType = 0,
    DebtValue = 1
}

enum AccountsProperty
{
    AccountNumber = 0,
    IsChekingAccount = 1,
    Owner = 2,
}

当然,这些可能都是相同的值。也就是说,虽然您无法控制排序顺序,但一旦给出了 属性 名称,手动版本就可以按照按名称排序的顺序声明手动编写的 lambda,这样相同的索引就会无论哪种方式都有效。你决定做什么并不重要;它必须是一致的。


最后的想法……

我是否提到过我强烈反对围绕此技术构建任何大量代码?根本不清楚您要解决的实际大局问题是什么,但是有很多不同的方法会出错,并且很可能会导致很多难以找到的问题,修复代码中的错误非常耗时。

就性能而言,只要您不在紧密循环中针对大量对象和 属性 值执行代码,上述内容应该不会太差。特别是手动(第一个)示例应该相对较快。通过使用 Expression 类型,可以以手动方法的最小开销实现基于反射方法的通用设计。这有点复杂,但优点是您可以动态生成表达式,最终有效地成为手动方法的编译代码实现。