本系列为阅读《Effective C#》一书所做的笔记,内容基本来自于该书籍与网上相关文档与博客;
由于主要起笔记作用,文章只记录了部分书籍中提到的优化要点,一些不常用的、本人暂时无法理解的、已经掌握或自以为已经掌握的内容不再做记录;
内容上比较简要,要获取具体的内容需要查阅相关的资料。
约束条件
示例
使用 Where T : XX 形式为泛型添加约束
// 泛型仅支持实现了 IComparable 接口的类型
public static bool AreEqual<T>(T left, T right) where T : IComparable<T> {
return left.CompareTo(right) == 0;
}
约束类型
refer to: C# 泛型约束
- T: struct:类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型
- T: class:类型参数必须是引用类型,包括任何类、接口、委托或数组类型
- T: new(): 类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定
- T: <基类名>:类型参数必须是指定的基类或派生自指定的基类
- T: <接口名>:类型参数必须是指定的接口或实现指定的接口,可以指定多个接口约束,约束接口也可以是泛型的
- T: U:为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数,这称为裸类型约束
使用优劣
优:
- 简化代码:例如未约束时可能需要判断参数类型,在运行时做类型转换,并对转换成功与否都做处理;通过合理的约束可以直接避免
- 性能提升:约束值类型时可以避免装箱拆箱
劣:
- 容易矫枉过正:添加过多约束后使该泛型类应用范围缩小
通过运行期类型检查来实现特定泛型算法
对于某些根据不同参数类型选择不同实现的泛型算法,可以通过运行期的类型检查来做判断,例如:
public class Test {
public static void Func<T>(ITestBase<T> param) {
if (param is ITest1<T>) {
// do sth
} else if (param is ITest2<T>) {
// do sth
} else {
// do sth
}
}
}
之所以使用运行期检查,而不是通过重载等编译期类型检查方式来实现对不同类型采取不同算法,是因为某些情况下,一些对象运行期与编译期的类型可能不一样(例如以 System.Object 作为方法参数)
考虑 IDisposable 接口
例:
public interface IEngine {
void DoWork();
}
public class Test<T> where T : IEngine, new() {
public void Func() {
T t = new T();
t.DoWork();
}
}
在 T 实现了 IDisposable 接口的时候,Func 的调用可能会导致资源泄露(比如未实现 finalizer 或未调用 Dispose),可以通过修改 Func 为如下形式来解决:
public class Test<T> where T : IEngine, new() {
public void Func() {
T t = new T();
using (t as IDisposable) {
t.DoWork();
}
}
}
using 语句
插入 using 语句的一点说明( using statement):
- 当某个实现了 IDisposable 接口的对象的生命周期被限定在某个方法中时,可以使用 using 语句来确保非托管资源的正确释放
- 在 using 块中,该对象为只读且无法修改或重新赋值
- 该对象最好在 using 语句中声明并实例化,编译器本身是支持在 using 外部进行声明的,但这样做的问题是离开 using 块后,对对象的访问就可能出错,例如:
Font font = new Font("Arial", 10.0f);
// not recommended
using(font) {
// do sth
}
// font is still in scope
// but the method call throws an exception
float f = font.GetHeight();
- using 语句确保了即使 using 块中抛出异常,依然能够正常调用 Dispose 接口,事实上,using 语句的实现如下:
Font font = new Font("Arial", 10.0f);
try
{
byte charset = font.GdiCharSet;
}
finally
{
if (font != null)
((IDisposable)font).Dispose();
}
IDisposable 对象作为泛型类的成员变量
除去上面的仅在某个方法中出现的 IDisposable 对象,某些情况下可能需要该对象作为泛型类的成员存在,此时,就需要泛型类本身实现 IDisposable 接口来确保非托管资源的正确释放:
// 这里使用 sealed 是因为如果有子类继承该类,则子类需要能够调用该类的 Dispose 接口来释放资源,
// 也即需要实现完整的 Dispose 模式,代码量较多,故通过禁止继承来减少代码,方便说明
public sealed class Test<T> : IDisposable where T : IEngine, new() {
private T t = new T();
public void Func() {
t.DoWork();
}
public void Dispose() {
var res = t as IDisposable;
res?.Dispose();
}
}
另一种方式是将 IDisposable 对象的管理权交给用户处理:
public class Test<T> where T : IEngine {
private T t;
public Test(T t) {
t = t;
}
public void Func() {
t.DoWork();
}
}
用委托要求类型参数必须提供某种方法
C# 中,泛型的约束方式并不那么广泛,对于比如要求泛型参数提供特定静态方法等约束并无法直接实现,只能迂回实现;此时除了常见的通过实现特定接口来约束,可以考虑通过委托来解决。
例如实现泛型加法的功能,可以有以下实现:
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
Console.WriteLine("Hello World");
Point a = new Point(1, 1);
Point b = new Point(2, 3);
Point c = Add(a, b);
Console.WriteLine("Point c: " + c.ToString());
}
public static T Add<T>(T a, T b) where T : IAdd<T> {
return a.Add(b);
}
}
public interface IAdd<T> {
T Add(T other);
}
public class Point : IAdd<Point> {
public int X { get; }
public int Y { get; }
public Point(int x, int y) {
X = x;
Y = y;
}
public Point Add(Point other) {
return new Point(this.X + other.X, this.Y + other.Y);
}
public override string ToString() {
return "(" + X + ", " + Y + ")";
}
}
以上通过定义一个 IAdd 接口,并要求泛型参数都实现 IAdd 接口来实现 Add 的功能,虽然可以正常使用,但缺陷是:
- 需要创建额外接口(IAdd)
- 实现功能的泛型方法需要加上 IAdd 的约束
- 泛型参数需要实现 IAdd 接口
而以上问题可以通过委托来解决:
using System;
using System.Collections.Generic;
public class Program {
public static void Main() {
Console.WriteLine("Hello World");
Point a = new Point(1, 1);
Point b = new Point(2, 3);
Point c = Add(a, b, (a1, b1) => new Point(a1.X + b1.X, a1.Y + b1.Y));
Console.WriteLine("Point c: " + c.ToString());
}
// addFunc: System.Func<T1, T2, TOutput>
public static T Add<T>(T a, T b, Func<T, T, T> addFunc) {
return addFunc(a, b);
}
}
public class Point {
public int X { get; }
public int Y { get; }
public Point(int x, int y) {
X = x;
Y = y;
}
public Point Add(Point other) {
return new Point(this.X + other.X, this.Y + other.Y);
}
public override string ToString() {
return "(" + X + ", " + Y + ")";
}
}
优先创建泛型方法而不是泛型类
泛型类的缺陷:
- 约束会施加在其所有方法上,缺乏灵活性
- 泛型方法可以在后续更方便的修改增加
- 每次 C# 都会对整个泛型类生成 IL,如果类型较多,则 IL 也会生成多份
故在特殊的必须使用到泛型类的情况之外,应当尽量使用泛型方法的实现(特别是对于工具类)
例如:
public class Program {
public static void Main() {
string str1 = "aaa";
string str2 = "bbb";
Console.WriteLine("string: {0}", Util<string>.Max(str1, str2));
double d1 = 1.00;
double d2 = 2.33;
Console.WriteLine("double: {0}", Util<double>.Max(d1, d2));
}
}
public static class Util<T> {
public static T Max(T left, T right) {
return Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
}
}
// output:
// string: bbb
// double: 2.33
上述代码的问题在于:
- 每次调用接口都需要指明泛型类的类型
- 对于 double 一类的数值类型,本身有类似 Math.Max() 之类的接口,采用上述代码后未能使用
改写为泛型方法如下:
public class Program {
public static void Main() {
string str1 = "aaa";
string str2 = "bbb";
Console.WriteLine("string: {0}", Util.Max(str1, str2));
double d1 = 1.00;
double d2 = 2.33;
Console.WriteLine("double: {0}", Util.Max(d1, d2));
}
}
public static class Util {
public static T Max<T>(T left, T right) {
return Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
}
public static double Max(double left, double right) {
return Math.Max(left, right);
}
}
// output:
// string: bbb
// double: 2.33
扩展方法
特点
- 扩展某个已有类型的功能,可以以调用该类型成员方法的形式调用扩展方法。ref to C# 扩展方法
- 需要定义包含扩展方法的静态类,扩展方法的定义也需要是静态的
- 方法的第一个参数指定方法所操作的类型,且此参数前面必须加上 this 修饰符
示例
using System.Linq;
using System.Text;
using System;
namespace CustomExtensions {
// Extension methods must be defined in a static class.
public static class StringExtension {
// This is the extension method.
// The first parameter takes the "this" modifier
// and specifies the type for which the method is defined.
public static int WordCount(this String str) {
return str.Split(new char[] {' ', '.','?'}, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
namespace Extension_Methods_Simple {
// Import the extension method namespace.
using CustomExtensions;
class Program {
static void Main(string[] args) {
string s = "The quick brown fox jumped over the lazy dog.";
// Call the method as if it were an
// instance method on the type. Note that the first
// parameter is not specified by the calling code.
int i = s.WordCount();
System.Console.WriteLine("Word count of s is {0}", i);
}
}
}
使用扩展方法的好处
- 可以与使用该类型的原生方法一样调用扩展方法
- 扩展某个类型的功能时,不需要通过继承并增加新方法来解决,扩展性强
注意点
- 同名的原生方法会屏蔽扩展方法,在实现时应进行避免或使二者的功能一致