前两期地址:
C#高效编程话题集2(每期10话题)
本期话题:
1:使用属性还是字段
首先重大区别就是属性实质是方法,所以:
1:可以为属性添加代码;
2:可以让属性支持线程安全;见effective c#第一版的第一章;
3:属性得到了VS编辑器的支持,得以实现自动属性这种功能。
4:自动属性的特点在LINQ中得到了广泛应用,尤其是匿名类型中,只能实现只读的自动属性,匿名类型不支持字段;
5:从设计的角度,也就是面向对象的角度,建议使用属性;
6:如果某个属性仅仅作为类型内部使用,而且不涉及到上面5点内容,则建议使用字段
还有一点,属性在序列化中有点尴尬,由于属性是方法,所以不能为其指定[NonSerialized]特性。
2:利用泛型拓宽方法的应用范围
假设存在方法:
static void PrintSalary(ISalary<Employee> s)
{
s.Pay();
}
然后像下面这样使用它:
ISalary<Programmer> s = new BaseSalaryCounter<Programmer>();
PrintSalary(s);
注意,几个主要类的代码:
interface ISalary<T>
{
void Pay();
}
class BaseSalaryCounter<T> : ISalary<T>
{
public void Pay()
{
Console.WriteLine("Pay base salary");
}
}
class Employee
{
public string Name { get; set; }
}
class Programmer : Employee
{
}
运行结果是:
无法从“ConsoleApplication4.ISalary<ConsoleApplication4.Programmer>”转换为“ConsoleApplication4.ISalary<ConsoleApplication4.Employee>”
显然我们认为PrintSalary方法因为能支持ISalary<Employee> ,所以肯定能支持ISalary<Programmer>,这是一种很容易犯的尝试错误。
改进方法先提供一种思路,就是将该方法改成泛型。
3:利用协变关键字out
要解决话题2中的问题,还可以使用out关键字。即修改接口为:
interface ISalary<out T>
{
void Pay();
}
4:减少使用类型的静态变量
1:静态变量一旦被创建不被释放;
2:静态变量不是线程安全的,它不像类型变量只对创建类型的那个线程有效;
5:线程同步有多少种方法?
所谓线程同步,采用的技术,就是在某个对象上等待(也理解为锁定该对象)。C#中类型对象的分类:引用类型和值类型。在这两种类型上的等待是不一样的,我们也可以简单的理解为在CLR中,值类型是不能被锁定的(参考MSDN的volatile关键字)。
在引用类型上的等待,又分为两种技术:锁定和信号机制。
锁定使用:关键字lock和类型Monitor,前者其实是后者的语法糖。这是最常用的同步技术,小伙们应该都用过;
信号机制:信号机制中涉及的类型都继承自抽象类WaitHandle,这些类型有EventWaitHandle(类型化为AutoResetEvent、ManualResetEvent)和Semaphore以及Mutex。它们之间有一定的区别,值得一提的是Mutex能同步不同应用程序域。其它几个类型的不同点MSDN写的很伤人,而且未进行横向比较,简单来说,就是EventWaitHandle通过布尔型进行同步,其中AutoResetEvent又自动恢复类型的阻滞状态,ManualResetEvent则必须手动恢复阻滞状态。Semaphore通过整型进行同步。
6:让对象=null能否加速对象被垃圾回收器回收?
注意,以下描述都只针对引用类型
1:在方法内,方法参数和局部变量,赋值为null无助于加速被垃圾回收器标识为垃圾;
2:实例变量,随着实例=null,实例变量也会自动=null。所以,一般情况下,无需显式为实例变量=null,除非你的实例生存周期较长,并且实例变量是个大对象;
3:静态变量,如果不显式置为null,就永远不会被回收;
7:不建议lock(this)
1:如果两个对象的实例分别执行了lock(this)这样的代码,实际锁定的是两个对象,完全不能达到同步的目的。
2:最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。
8:区分计算密集型和I/O密集型的多线程应用场景
I/O密集型操作。硬盘、网卡、声卡、显卡等都是。CLR所提供的异步编程模型就是让我们充分利用硬件的DMA功能来提高CPU的利用率。
一个在大多数情况下正确的技巧是,凡是FCL中类型提供了类似BeginDoSomething方法的,都建议使用这个异步调用来完成多线程编码,异步在后台调用线程池线程完成调度,最大化的节约了系统的性能。
9:使用Parallel的一个陷阱
以下的代码的输出是什么?
int[] nums = new int[] { 1, 2, 3, 4 };
long total = 0;
Parallel.For<long>(0, nums.Length, () =>
{
return 1;
}, (i, loopState, subtotal) =>
{
subtotal += nums[i];
return subtotal;
},
(x) => Interlocked.Add(ref total, x)
);
Console.WriteLine("The total is {0}", total);
实际上,它有可能是11,较少的情况下会是12,几乎不可能出现13,14。
要从方法的最后一个参数Action<TLocal> localFinally讲起,它表示的其实是并行中如果新起了一个线程,那么这个线程结束时会调用这个localFinally。 而我们知道并行在后台采用的是线程池,也就是,我们不知道上面代码所发起的这个并行实际会发起多少个线程,如果是1个,自然就是11,如果是2,结果自然就是12了,依此类推。之所以不可能为14,是因为,并发的循环体就只有4个循环,并发显然不会傻到新起4个线程来执行并发。
10:不建议lock(类型的type)
由于typeof(SampleClass),是SampleClass的所有实例所共有的,这会导致当前应用程序中的所有SampleClass的实例的线程,将会全部被同步。
更多内容,期待下一期。。。