替代反射的方案

3

我在泛型和反射方面的经验很少。从下面的示例中,我认为执行它需要太长时间。有没有一种方法可以在不使用反射的情况下完成以下操作。

情景 我正在处理一个通用方法。它接受一个传递给它的类实例,并从所有属性中创建SqlParameters。以下是名为“Store”的通用方法的代码,以及将C#类型转换为DbType的SqlDbType的另一种方法。

        List<SqlParameter> parameters = new List<SqlParameter>();
        public T Store<T>(T t)
        {
            Type type = t.GetType();
            PropertyInfo[] props = (t.GetType()).GetProperties();
            foreach (PropertyInfo p in props)
            {
                SqlParameter param = new SqlParameter();
                Type propType = p.PropertyType;
                if (propType.BaseType.Name.Equals("ValueType") || propType.BaseType.Name.Equals("Array"))
                {
                    param.SqlDbType = GetDBType(propType); //e.g. public bool enabled{get;set;} OR public byte[] img{get;set;}
                }
                else if (propType.BaseType.Name.Equals("Object"))
                {
                    if (propType.Name.Equals("String"))// for string values
                        param.SqlDbType = GetDBType(propType);
                    else
                    {
                        dynamic d = p.GetValue(t, null); // for referrences e.g. public ClassA obj{get;set;}
                        Store<dynamic>(d);
                    }
                }
                param.ParameterName = p.Name;
                parameters.Add(param);
            }
            return t;
        }



        // mehthod for getting the DbType OR SqlDbType from the type...
        private SqlDbType GetDBType(System.Type type)
        {
            SqlParameter param;
            System.ComponentModel.TypeConverter tc;
            param = new SqlParameter();
            tc = System.ComponentModel.TypeDescriptor.GetConverter(param.DbType);
            if (tc.CanConvertFrom(type))
            {
                param.DbType = (DbType)tc.ConvertFrom(type.Name);
            }
            else
            {
                // try to forcefully convert
                try
                {
                    param.DbType = (DbType)tc.ConvertFrom(type.Name);
                }
                catch (Exception e)
                {
                    switch (type.Name)
                    {
                        case "Char":
                            param.SqlDbType = SqlDbType.Char;
                            break;
                        case "SByte":
                            param.SqlDbType = SqlDbType.SmallInt;
                            break;
                        case "UInt16":
                            param.SqlDbType = SqlDbType.SmallInt;
                            break;
                        case "UInt32":
                            param.SqlDbType = SqlDbType.Int;
                            break;
                        case "UInt64":
                            param.SqlDbType = SqlDbType.Decimal;
                            break;
                        case "Byte[]":
                            param.SqlDbType = SqlDbType.Binary;
                            break;
                    }
                }
            }
            return param.SqlDbType;
        }

调用我的方法,假设我有以下 2 个类:
public class clsParent
{
    public int pID { get; set; }
    public byte[] pImage { get; set; }
    public string pName { get; set; }
}

and

public class clsChild
{
    public decimal childId { get; set; }
    public string childName { get; set; }
    public clsParent parent { get; set; }
}

and this is a call 


clsParent p = new clsParent();
p.pID = 101;
p.pImage = new byte[1000];
p.pName = "John";
clsChild c = new clsChild();
c.childId = 1;
c.childName = "a";
c.parent = p;

Store<clsChild>(c);

1
由于您正在迭代未知类型的属性并获取有关它们的信息,我认为您无法避免使用反射,实际上,您当前使用泛型似乎没有任何作用。您可以将该方法改为接受一个“object”参数:public void Store(object t) - JLRishe
@JLRishe 这不是我想要从该方法实现的完整版本,但是从这个方法中可以清楚地处理未知类型的方式... 我已经为唯一的问题“时间”搜索了很多,但没有找到任何解决方案... 我想在此方法中使用ADO.Net。 - Abdul Majid
1
@AbdulMajid:这不是一个替代方案,只是一个建议:如果类型在运行时重复存储,您可以尝试通过引入一些缓存来调整反射方法。您可以缓存从Type.GetProperties()返回的数组。对于分配或转换值的部分,您可以尝试在Func<>实例中缓存逻辑并重用它们。 - Theo Lenndorff
@AbdulMajid: 无法将示例放入评论中,因此我将其发布为答案。对于 Func<> 方法,我没有想出适合您需求的好示例。所以我将其省略了。 - Theo Lenndorff
@TheoLenndorff 在回答中或者Gist中提供示例? - Kiquenet
5个回答

3
如果你想要消除反射,可以在以下代码中找到灵感。
这里所有与存储在数据库中的对象的访问以及sql属性值分配的操作都由从数据类型编译的运行时表达式处理。
假设包含值的表名为`test`,字段名假定与属性值相同。
对于每个属性,构建一个`Mapping`。它将包含一个包含数据库字段的`FieldName`,一个`SqlParameter`,该参数应正确插入SQL `INSERT`语句(示例在`main`中),最后如果包含编译的动作,可以接受输入`T`对象的实例并将值分配给`SqlParameters`属性`Value`。这些映射的集合构造在`Mapper`类中。为了解释,代码是内联的。
最后,`main`方法展示了如何将所有东西绑定在一起。
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;

namespace ExpTest
{
    class Program
    {
        public class Mapping<T>
        {
            public Mapping(string fieldname, SqlParameter sqlParameter, Action<T, SqlParameter> assigner)
            {
                FieldName = fieldname;
                SqlParameter = sqlParameter;
                SqlParameterAssignment = assigner;
            }
            public string FieldName { get; private set; }
            public SqlParameter SqlParameter { get; private set; }
            public Action<T, SqlParameter> SqlParameterAssignment { get; private set; }
        }

        public class Mapper<T>
        {
            public IEnumerable<Mapping<T>> GetMappingElements()
            {
                foreach (var reflectionProperty in typeof(T).GetProperties())
                {
                    // Input parameters to the created assignment action
                    var accessor = Expression.Parameter(typeof(T), "input");
                    var sqlParmAccessor = Expression.Parameter(typeof(SqlParameter), "sqlParm");

                    // Access the property (compiled later, but use reflection to locate property)
                    var property = Expression.Property(accessor, reflectionProperty);

                    // Cast the property to ensure it is assignable to SqlProperty.Value 
                    // Should contain branching for DBNull.Value when property == null
                    var castPropertyToObject = Expression.Convert(property, typeof(object));


                    // The sql parameter
                    var sqlParm = new SqlParameter(reflectionProperty.Name, null);

                    // input parameter for assignment action
                    var sqlValueProp = Expression.Property(sqlParmAccessor, "Value");

                    // Expression assigning the retrieved property from input object 
                    // to the sql parameters 'Value' property
                    var dbnull = Expression.Constant(DBNull.Value);
                    var coalesce = Expression.Coalesce(castPropertyToObject, dbnull);
                    var assign = Expression.Assign(sqlValueProp, coalesce);

                    // Compile into action (removes reflection and makes real CLR object)
                    var assigner = Expression.Lambda<Action<T, SqlParameter>>(assign, accessor, sqlParmAccessor).Compile();

                    yield return
                        new Mapping<T>(reflectionProperty.Name, // Table name
                            sqlParm, // The constructed sql parameter
                            assigner); // The action assigning from the input <T> 

                }
            }
        }

        public static void Main(string[] args)
        {
            var sqlStuff = (new Mapper<Data>().GetMappingElements()).ToList();

            var sqlFieldsList = string.Join(", ", sqlStuff.Select(x => x.FieldName));
            var sqlValuesList = string.Join(", ", sqlStuff.Select(x => '@' + x.SqlParameter.ParameterName));

            var sqlStmt = string.Format("INSERT INTO test ({0}) VALUES ({1})", sqlFieldsList, sqlValuesList);

            var dataObjects = Enumerable.Range(1, 100).Select(id => new Data { Foo = 1.0 / id, ID = id, Title = null });

            var sw = Stopwatch.StartNew();

            using (SqlConnection cnn = new SqlConnection(@"server=.\sqlexpress;database=test;integrated security=SSPI"))
            {
                cnn.Open();

                SqlCommand cmd = new SqlCommand(sqlStmt, cnn);
                cmd.Parameters.AddRange(sqlStuff.Select(x => x.SqlParameter).ToArray());

                dataObjects.ToList()
                    .ForEach(dto =>
                        {
                            sqlStuff.ForEach(x => x.SqlParameterAssignment(dto, x.SqlParameter));
                            cmd.ExecuteNonQuery();
                        });
            }


            Console.WriteLine("Done in: " + sw.Elapsed);
        }
    }

    public class Data
    {
        public string Title { get; set; }
        public int ID { get; set; }
        public double Foo { get; set; }
    }
}

我将这个作为一个单独的答案,因为它包含了一个相当复杂的尝试,使用 LinqExpression 命名空间来赋值。 - faester
@AbdulMajid,你完成代码后会将其粘贴吗? - Kiquenet

2

有人告诉你反射会导致性能问题,但你并没有真正通过分析器运行代码。

我试了一下你的代码,它花费了18毫秒(65000个时钟周期)运行,相对于将数据保存在数据库中所需的时间来说,它相当快。 但你说的确实是太耗时间了。 我发现当转换Byte[]时,你的代码在调用tc.ConvertFrom时引发了一个异常。 将clsParent中的byte[] pImage移除后,运行时间降至850个时钟周期。

这里的性能问题是由异常引起的,而不是反射。

我已经修改了你的GetDBType:

    private SqlDbType GetDBType(System.Type type)
    {
        SqlParameter param;
        System.ComponentModel.TypeConverter tc;
        param = new SqlParameter();
        tc = System.ComponentModel.TypeDescriptor.GetConverter(param.DbType);
        if (tc.CanConvertFrom(type))
        {
            param.DbType = (DbType)tc.ConvertFrom(type.Name);
        }
        else
        {
            switch (type.Name)
            {
                case "Char":
                    param.SqlDbType = SqlDbType.Char;
                    break;
                case "SByte":
                    param.SqlDbType = SqlDbType.SmallInt;
                    break;
                case "UInt16":
                    param.SqlDbType = SqlDbType.SmallInt;
                    break;
                case "UInt32":
                    param.SqlDbType = SqlDbType.Int;
                    break;
                case "UInt64":
                    param.SqlDbType = SqlDbType.Decimal;
                    break;
                case "Byte[]":
                    param.SqlDbType = SqlDbType.Binary;
                    break;

                default:
                    try
                    {
                        param.DbType = (DbType)tc.ConvertFrom(type.Name);
                    }
                    catch
                    {
                        // Some error handling
                    }
                    break;
            }
        }
        return param.SqlDbType;
    }

我希望这可以帮助您在IT技术方面的探索中更上一层楼。


谢谢,最耗时的是以下这行代码 dynamic d = p.GetValue(t, null); 实际上没有人告诉我这段代码很慢,但我从运行所需的时间推断出来了。 - Abdul Majid
1
我真的说不清楚,我使用了 object d = p.GetValue(t, null),效果非常好。由于我在VS 2010中工作,所以我对动态关键字的性能一无所知。 - Casperah
这是一个晚期的评论,但是与使用“对象”相比,使用“动态”变量会在运行时执行大量工作,因此具有开销。Marc Gravell的FastMember库值得一看 https://code.google.com/p/fast-member/ - Surya Pratap

2

这不是一个替代方案,只是一个建议:如果类型在运行时重复存储,您可以尝试通过引入一些缓存来调整反射方法。

而不是:

PropertyInfo[] props = (t.GetType()).GetProperties();

尝试使用以下缓存方法:
PropertyInfo[] props = GetProperties(type);

其中GetProperties(Type)的实现如下:

private Dictionary<Type, PropertyInfo[]> propertyCache;
// ...
public PropertyInfo[] GetProperties(Type t)
{
    if (propertyCache.ContainsKey(t))
    {
        return propertyCache[t];
    }
    else
    {
        var propertyInfos = t.GetProperties();
        propertyCache[t] = propertyInfos;
        return propertyInfos;
    }
}

以下是如何缓存 Type.GetProperties() 方法调用的方法。您可以对代码中其他部分使用相同的方法。例如,您使用 param.DbType = (DbType)tc.ConvertFrom(type.Name); 的地方也可以进行替换。您还可以使用查找表来替换 if 和 switch 语句。但在执行此类操作之前,请务必进行一些性能分析。这会使代码变得更加复杂,如果没有充分的理由,请勿这样做。


2

我认为你通常会受益于使用标准ORM,例如NHibernateEntity Framework。这两个都可以从类到关系数据库进行(可定制)映射,而NHibernate则可以在所有标准DBMS系统之间提供完全的灵活性。

话虽如此,你应该能够通过使用Linq表达式来获得部分功能,这些表达式稍后可以编译;这应该会给你更好的性能。


我不想使用NHibernate、Entity Framework或LINQ to SQL,只是因为我唯一对它们有意见的问题是执行简单操作所需的时间太长了。我的目标是使用简单的ADO.Net将我的类映射。 - Abdul Majid
@AbdulMajid,这完全取决于你的决定。我尝试创建了一种编译的SqlParameter值分配方法,并将其发布为单独的答案。它很快变得棘手,但是可行的。 :) - faester

0

除非使用后期构建处理重写代码,否则没有替代反射的方法。 如果仅将其用于准备动态发出的类型/方法/委托并将其自然地包含为策略模式,则可以使用反射而不会出现任何性能问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接