CLR类型设计之泛型(二)

时间:2023-03-09 06:48:56
CLR类型设计之泛型(二)

在上一篇文章中,介绍了什么是泛型,以及泛型和非泛型的区别,这篇文章主要讲一些泛型的高级用法,泛型方法,泛型接口和泛型委托,协变和逆变泛型类型参数和约束性,泛型的高级用法在平时的业务中用的不多,多用于封装高级方法和一些底层封装,前几天读了一篇文章,如何选择网络上的技术文章,因为现在关于技术的文章可以说非常多,但是时间是有限的,如果花很多时间阅读了一篇文章却没有什么用,岂不是很浪费时间,所以第一步选择自己感兴趣的文章阅读,第二要把阅读过的文章尽可能实现一次,读书万遍不如走上一遍,第三尽量不读翻译性的文章,这里其实我觉得不是所有人都能很轻松的看懂官方文档,所以这点还是仁者见仁,智者见智。为了让文章尽可能的有深度,所以我觉得以后的博文中应该尽可能的贴出的知识模块都有所解释,有所理解。不是在网上复制粘贴以后在给别人看,博文不是笔记,所以要说出自己的见解

说了这么多,那么就开始务实的甩开膀子做吧!

泛型方法

既然讲到了泛型是为了高级封装,那么我们就来封装一个C#中的ORM吧,在封装ORM之前还要有一个SQL帮助类,这个网上有很多,感兴趣的可以直接到网上找一个,C#中封装的ORM最好的Entity FromWork,感兴趣的可以看下源码,我们先看下下面的代码,这是一个典型的泛型方法

     /// <summary>
/// 查询获得一个实体
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sql"></param>
/// <param name="sqlParameters"></param>
/// <param name="transaction"></param>
/// <returns></returns>
public static T Get<T>(string sql, IList<SqlParameter> sqlParameters = null, IDbTransaction transaction = null)
{
DataTable table = SQLServerHelper.Query(sql, sqlParameters, transaction);
if (table != null && table.Rows != null && table.Rows.Count > )
{
DataRow row = table.Rows[];
return ConvertRowToModel<T>(row, table.Columns);
}
else
{
Type modeType = typeof(T);
return default(T);
} }

注释已经表明了这是用来获取一个实体的Get<T>()方法中T定义为泛型类型,方法有一个必选参数两个默认参数,string类型的sql语句,默认IList<SqlParameter> sqlParameters继承自IList集合的SQL参数。这是参数化SQL的,每一个SQL语句都应该写成参数化的,还有一个IDbTransaction transaction = null是否开启事务,有了这样三个参数就可以定义一个查询指定SQL语句,是否有Where条件,是否开启事务的方法,方法继续执行,第是一行代码是使用帮助类获得数据,第14行和第15行是将获得数据映射成对应的结构体,我们可以看下 ConvertRowToModel<T>(row, table.Columns)的内部实现

   public static T ConvertRowToModel<T>(DataRow row, DataColumnCollection columns)
{
Type modeType = typeof(T);
Object model = Activator.CreateInstance(modeType); foreach (var p in model.GetType().GetProperties())
{
var propertyName = p.Name.ToUpper();
var propertyType = p.PropertyType.Name;
if (columns.Contains(propertyName))
{
var value = row[propertyName]; if (propertyType.ToUpper().Contains("STRING"))
{ if (Convert.IsDBNull(value))
{
value = string.Empty;
}
else
{
p.SetValue(model, value.ToString(), null);
}
}
else if (propertyType.ToUpper().Contains("INT"))
{ if (Convert.IsDBNull(value))
{
value = ;
} p.SetValue(model, Int32.Parse(value.ToString()), null);
}
else if (propertyType.ToUpper().Contains("SINGLE"))
{ if (Convert.IsDBNull(value))
{
value = 0.0f;
}
p.SetValue(model, Single.Parse(value.ToString()), null);
}
else if (propertyType.ToUpper().Contains("DATETIME"))
{ if (Convert.IsDBNull(value))
{
value = DateTime.MinValue;
}
p.SetValue(model, DateTime.Parse(value.ToString()), null);
}
else if (propertyType.ToUpper().Contains("DOUBLE"))
{ if (Convert.IsDBNull(value))
{
value = 0.0d;
}
p.SetValue(model, Double.Parse(value.ToString()), null);
}
else if (propertyType.ToUpper().Contains("BOOLEAN"))
{ if (Convert.IsDBNull(value))
{
value = false;
}
if (value.GetType() == typeof(Int32))
{
p.SetValue(model, Int32.Parse(value.ToString()) == , null); }
else if (value.GetType() == typeof(String))
{
p.SetValue(model, Boolean.Parse(value.ToString()), null);
}
else if (value.GetType() == typeof(Boolean))
{
p.SetValue(model, (Boolean)(value), null);
} }
else if (p.PropertyType.IsEnum)//Enum
{
if (Convert.IsDBNull(value) || string.IsNullOrEmpty(value.ToString()))
{
value = "";
} p.SetValue(model, int.Parse(value.ToString()), null);
}
else if (propertyType.ToUpper().Contains("DECIMAL"))
{ if (Convert.IsDBNull(value))
{
value = 0.0f;
}
p.SetValue(model, Decimal.Parse(value.ToString()), null);
} }
}
return (T)model;
}

这个方法有点长,但实际上很好理解,并且这个方法也是一个泛型方法,其中的关键点在于他会把所有的字段类型转换成基础的元类型,转换成int,string这些最终会在返回给Get<T>()方法,这样就完成了实体的映射,那么有了上面两个方法,就可以编写简单的ORM了,如果是增删查改的话,就改变其中的逻辑,获得返回的影响行数就可以了,那么如何调用这个ORM呢,封装后最主要的是使用。可以用如下方法直接调用

     public static Student Getmodel(long id) {
StringBuilder sql = new StringBuilder();
List<SqlParameter> args = new List<SqlParameter>();
//根据学生id获取学生信息
sql.Append("select * from student where id=@id");
//id参数化后赋值
args.Add(new SqlParameter("@id", id));
//调用封装ORM所在的CommonDao类 调用刚才的Get方法
//映射类Student,就会返回和Student类相符合的数据库字段内容
return CommonDao.Get<Student>(sql.ToString(), args);
}

是不是就简单了很多呢。当然你也可以封装的更深一些,这里还是在传递sql语句,如果你喜欢Entitie fromwork那种方式,可以把Sql语句也作为固定的写法,只传递where条件后面的参数就可以了,可以看到泛型方法很有用处

但是有的时候定义了泛型方法,却希望他只能用于某一种特定的类型,上述的例子可以用于所有泛型类型,但是如果我要封装的不是底层,只是某一个高级方法,只允许某一种类型使用这个方法,那么该如何做呢?可以使用泛型约束

泛型约束

  public static T WhereGet<T>(string sql, IList<SqlParameter> sqlParameters = null, IDbTransaction transaction = null)
where T:IList<T>
{
DataTable table = SQLServerHelper.Query(sql, sqlParameters, transaction);
if (table != null && table.Rows != null && table.Rows.Count > )
{
DataRow row = table.Rows[];
return ConvertRowToModel<T>(row, table.Columns);
}
else
{
Type modeType = typeof(T);
return default(T);
} }

上面的代码示例,可以看到和第一次的写法上一样的,只是行2处多加了一个where T:IList<T>,这就限制了T只能是实现了IList接口的集合访问。可以看到泛型方法很有用处,写一个方法可以适应很多类型,避免了同一套逻辑下的不同类型参数的重载。而且泛型不仅仅支持方法,还支持接口和委托

泛型接口和泛型委托

     目的是一样的,只是一种拓展,将泛型用于接口和委托上,但是如果没有泛型接口,那么每次用非泛型接口来操作值类型都会发生一次装箱操作,因此C#提供了泛型接口的支持,来看下C#中提供的IList泛型接口

    //
// 摘要:
// 表示可按照索引单独访问的一组对象。
//
// 类型参数:
// T:
// 列表中元素的类型。
[DefaultMember("Item")]
[TypeDependencyAttribute("System.SZArrayHelper")]
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
//
// 摘要:
// 获取或设置位于指定索引处的元素。
//
// 参数:
// index:
// 要获得或设置的元素从零开始的索引。
//
// 返回结果:
// 位于指定索引处的元素。
//
// 异常:
// T:System.ArgumentOutOfRangeException:
// index 不是 System.Collections.Generic.IList`1 中的有效索引。
//
// T:System.NotSupportedException:
// 设置该属性,而且 System.Collections.Generic.IList`1 为只读。
T this[int index] { get; set; } //
// 摘要:
// 确定 System.Collections.Generic.IList`1 中特定项的索引。
//
// 参数:
// item:
// 要在 System.Collections.Generic.IList`1 中定位的对象。
//
// 返回结果:
// 如果在列表中找到,则为 item 的索引;否则为 -1。
int IndexOf(T item);
//
// 摘要:
// 将一个项插入指定索引处的 System.Collections.Generic.IList`1。
//
// 参数:
// index:
// 从零开始的索引,应在该位置插入 item。
//
// item:
// 要插入到 System.Collections.Generic.IList`1 中的对象。
//
// 异常:
// T:System.ArgumentOutOfRangeException:
// index 不是 System.Collections.Generic.IList`1 中的有效索引。
//
// T:System.NotSupportedException:
// System.Collections.Generic.IList`1 为只读。
void Insert(int index, T item);
//
// 摘要:
// 移除指定索引处的 System.Collections.Generic.IList`1 项。
//
// 参数:
// index:
// 要移除的项的从零开始的索引。
//
// 异常:
// T:System.ArgumentOutOfRangeException:
// index 不是 System.Collections.Generic.IList`1 中的有效索引。
//
// T:System.NotSupportedException:
// System.Collections.Generic.IList`1 为只读。
void RemoveAt(int index);
}

我们平时所写的List集合,其实都默认实现了IList接口的行为,在下一篇文章接口中,我们就重点讨论接口的行为,这里先不研究这个问题,先只看IList为什么是泛型接口,它实现了什么?行10中 :public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable这句话表明了IList是一个接口,可访问性公开继承了ICollection,IEnumerable,IEnumerable的接口行为和方法,里面的代码是其中的具体实现,比如行29:T this[int index] { get; set; },这个是被封装成了属性的方法,从返回结果中可以看到这个属性是为了实现获取位于指定索引处的元素。用法其实就是List.Index('要获得或设置的元素'),这就是C#中实现的一个泛型接口,那么泛型委托又是什么呢?

CLR支持泛型委托,目的是保证任何类型对象都能以类型安全的方式传给回掉方法,此外泛型委托允许值类型实例在传给回掉方法时不进行任何装箱,在C#中委托实际只提供了四个方法和一个类定义,4个方法包括一个构造器,一个Invoke方法,一个BeginInvoke方法和EndInvoke方法,如果定义的委托类型指定了类型参数,编译器会定义委托类的方法,用指定的参数类型替换方法的参数类型和返回值类型。

委托最大的作用就是为类的时间绑定事件处理程序,但这和泛型委托没关系,以后的文章中会有关于委托的详细介绍,示例

     //
// 摘要:
// 基于谓词筛选值序列。
//
// 参数:
// source:
// 要筛选的 System.Collections.Generic.IEnumerable`1。
//
// predicate:
// 用于测试每个元素是否满足条件的函数。
//
// 类型参数:
// TSource:
// source 中的元素的类型。
//
// 返回结果:
// 一个 System.Collections.Generic.IEnumerable`1,包含输入序列中满足条件的元素。
//
// 异常:
// T:System.ArgumentNullException:
// source 或 predicate 为 null。
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

上述代码看起来有点眼熟?没错这个就是Linq中得where。实际上Linq中得Where就是使用委托实现Func关键字就代表了委托

委托和接口的逆变和协变泛型类型实参

委托的每个泛型类型参数都可标记为协变量或逆变量,利用这个功能可将泛型委托类型的变量转换为相同的委托类型,可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的。我们举一个例子,

List<汽车> 一群汽车 = new List<汽车>();

       List<车子> 一群车子 = 一群汽车;
      显然,上面那段代码是会报错的, 虽然汽车继承于车子,可以隐士转换为车子,但是List<汽车>并不继承于List<车子>,所以上面的转换,是行不通的。

IEnumerable<汽车> 一群汽车 = new List<汽车>();

          IEnumerable<车子> 一群车子 = 一群汽车;
          然而这样却是可以的。那么IEnumerable接口有什么不同呢,更高级?但是IList继承自IEnumerable,IEnumerable有的IList应该都有,但是IList有的IEnumerable不一定有,那么为啥IEnumerable能实现这种转化?跟踪到 IEnumerable接口发现其定义是这样的  public interface IEnumerable<out T>,T前面多了一个out,实际上是这个参数起了作用,在T前面声明out代表这是一个协变变量,声明in代表这是一个逆变变量,使用“out”,和“in”两个关键字。但是只能用在接口和委托上面,对泛型的类型进行声明,当声明为“out”时,代表它是用来返回的,只能作为结果返回,中途不能更改。当声明为"in"时,代表它是用来输入的,只能作为参数输入,不能被返回。
           “协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型。
            “逆变”则是指能够使用派生程度更小的类型。逆变,逆于常规的变。
           回到上面的例子,正因为“IEnumerable”接口声明了out,所以,代表参数T只能被返回,中途不会被修改,所以,IEnumerable<车子> 一群车子 = 一群汽车;  这样的强制转换
 
是合法的,IL中实际上是作了强制转换的。

总结:

泛型可以说是一个非常重要的概念和点,在这里有很多知识可以学习,但学习和实际练习还是有一定区别的,所以还是要多写一些代码辅助理解