摘自陆敏技之《编写高质量代码:改善C#程序的157个建议》,编写C#程序代码时应考虑代码效率、安全和美观,可参考下述建议。想成为一名合格的搬砖工,牢记吧!!
1、正确操作字符串
1) 避免装箱操作。如语句:String str = "hans"+8 就存在装箱操作,建议改成语句:String str = "hans"+8.ToString()
2) 使用StringBuilder代替String运算(经测试,当执行5000次加运算时,StringBuilder效率是String的近倍)。C#中String一旦被赋值不可改变,进行任何操作(+,=)都会在内存中创建一个新的字符串对象,会给运行计算带来额外开销。而StringBuilder并不会重新创建一个新的String对象,StringBudiler每次执行+操作时,如果内容空间不够(默认长度16),会重新加倍进行分配空间。
//耗时3000毫秒
String str = "";
for (int i = ; i < ; i++) {
str += i.ToString();
} //耗时5毫秒
StringBuilder sb = new StringBuilder();
for (int i = ; i < ; i++)
{
sb.Append(i.ToString());
}
3) 常使用String.Format方法。 String.Format在内部使用了StringBuilder方法格式化字符串,效率很高,且代码美观,可读性高。
2、类型转换
1) 常使用as转换类型。as转换类型效率高,类型转换失败且不会报异常,而是值为null。
2) 使用TryParse代替Parse。Parse转型失败会引发异常,异常过程会消耗性能,而TryParse转型失败无异常,out操作符将参数设置为0。经测试,当转型失败时,TryParse效率要比Parse高几百倍。
3) 使用int?使得值类型也可以为nul。T?是Nullable<T>的简写,值可以为nul。T?判断是否为nul可用简写操作符??,如int? i=22;int j = i ?? 0。
3、区别readonly和const使用方法。const是一个编译期常量,readonly是一个运行时常量。const在编译时,会将常量用对应的值替代,运行效率高,而readonly在运行时初始化,初始化后不可修改,运行效率比const低,但是灵活性高。readonly赋值发生在运行时,赋值后不可改变表示:1) 值类型,只本身不可改变 2) 引用类型,引用指针不可改变,即不可修改指针指向新对象,但对象内容可修改。
4、避免给enum枚举类型的元素提供显示的值。在如下枚举类型Week中增加一个元素,输出ValueTemp的值等于Wednesday,原因是ValueTemp定义时没有赋值,编译期会逐个为元素值+1,当编译器发现ValueTemp时,会在Tuesday = 2的基础上+1,所以ValueTemp实际赋值为3,与Wednesday=3相等。
enum Week { Monday = 1, Tuesday = 2, ValueTemp, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7 }
5、熟悉运算符重载。运算符重载可使得对象运算操作简洁方便,参考下例:
class Salary
{
public int RMB { get; set; } //运算符重载
public static Salary operator +(Salary s1, Salary s2) {
s2.RMB += s1.RMB;
return s2;
}
} //调用
Salary s1 = new Salary() { RMB = };
Salary s2 = new Salary() { RMB = };
Salary s3 = s1 + s2; //运用运算符重载
6、实现深拷贝和浅拷贝。浅拷贝是将对象中的所有字段复制到新的对象中,其中,值类型字段拷贝后副本的修改不会影响源对象对应值,而引用字段拷贝后与原字段指向同一对象地址,副本值修改后会影响源对象对应的值(参考《C#值类型与引用类型区别》);深拷贝是将对象中的所有值类型和引用类型字段复制到新对象中,引用类型字段重新创建引用对象,副本的修改不影响源对象对应值。
//浅拷贝 继承ICloneable接口并实现Clone方法
class Salary : ICloneable
{
public int RMB { get; set; } public object Clone()
{
//实现浅拷贝
return this.MemberwiseClone();
}
} //深拷贝,通过对象序列化和反序列化实现,继承接口ICloneable并实现方法Clone
[Serializable]
class Salary : ICloneable
{
public int RMB { get; set; } public object Clone()
{
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as Salary;
}
}
}
7、使用dynamic简化反射操作。 dynamic是Framework 4.0的新特性,可以使C#具有弱语言的特性。
1) var与dynamic的区别:var在编译的时候替换成自动匹配的实际类型,而dynamic被编译后实际上是一个Object类型,只是编译期会进行特殊处理,在编译器不进行任何的类型检查,而是将类型检查放到了运行期。
2) 反射的优化,参考下例:
class A
{
public String Name { get; set; } public int Add(int a, int b)
{
return a + b;
}
} //调用
A a1 = new A();
MethodInfo m = a1.GetType().GetMethod("Add"); //普通反射 耗时1084ms
for (int i = ; i < ; i++)
{
int re = (int)m.Invoke(a1, new object[] { , });
} //优化后的反射 耗时13ms
var delg = (Func<A, int, int, int>)Delegate.CreateDelegate(typeof(Func<A, int, int, int>), m);
for (int i = ; i < ; i++)
{
delg(a1, , );
} //使用dynamic优化反射 耗时60ms
dynamic a2 = new A();
for (int i = ; i < ; i++) {
a2.Add(, );
} //使用dynamic优化反射 耗时60ms
dynamic a2 = new A();
for (int i = ; i < ; i++) {
a2.Add(, );
}
8、使用Environment.NewLine获取当前环境下的换行符号。
9、使用params减少重复参数。如方法:public void pap(String a,String b,String c){ } 可简写为:public void pap(params String[] args){ }。注意:① params数组必须是方法的最后一个参数 ② 不允许out或ref数组
10、扩展类型中的方法。扩展方法是一种特殊的静态方法,可以为类型扩展方法而无需创建新的派生类型。本实例扩展了String类型添加了扩展方法Test():
//自定义扩展类 必须为静态类
static class StringExtenstion
{
//扩展String类添加方法Test,必须为静态方法,参数格式为:this 类型名称 对象
//调用方法如:String str="hans"; Console.WriteLine(str.Test()); --输出my string
public static String Test(this String str) {
return "my string";
}
}
1、对象和集合初始化:Person person = new Person(){ Name="hans",Age = 25 };
2、匿名类型:var persion = new { Name="hans",Age=25 }; 编译器会自动生成具有对应字段的匿名类。
3、LINQ查询中避免不必要的迭代。充分运用First和Take等方法,查询到符合条件的记录就立即返回,而不是所有结果返回再筛选,效率可大幅度提高。
1、继承IDispose接口的类型,实例化可用using语法。using会在结束时,自动调用对象的Dispose方法。
2、通用BinarySerializer序列化。BinarySerializer.cs:
class BinarySerializer
{ //将类型序列化为字符串
public static string Serialize<T>(T t)
{
using (MemoryStream stream = new MemoryStream()) {
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, t);
return System.Text.Encoding.UTF8.GetString(stream.ToArray());
}
} //将类型序列化为文件
public static void SerializeToFile<T>(T t, string path, string fullName)
{
if (!Directory.Exists(path)) {
Directory.CreateDirectory(path);
}
string fullPath = string.Format(@"{0}\{1}", path, fullName);
using (FileStream stream = new FileStream(fullPath, FileMode.OpenOrCreate)) {
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, t);
stream.Flush();
}
} //将字符串反序列化为类型
public static TResult Deserialize<TResult>(string s) where TResult : class
{
byte[] bs = System.Text.Encoding.UTF8.GetBytes(s);
using (MemoryStream stream = new MemoryStream(bs)) {
BinaryFormatter formatter = new BinaryFormatter();
return formatter.Deserialize(stream) as TResult;
}
} //将文件反序列化为类型
public static TResult DeserializeFromFile<TResult>(string path) where TResult : class
{
using (FileStream stream = new FileStream(path, FileMode.Open)) {
BinaryFormatter formatter = new BinaryFormatter();
return formatter.Deserialize(stream) as TResult;
}
}
}
3、序列化特性说明:
1) Serializable:用于类,指示一个类可以序列化;
2) NonSerialized:用于字段,指示一个字段不被序列化。因为属性的本质是方法,因此NonSerialized不可直接用于属性,可用于自己实现的属性;
3) OnDeserialized:应用于某方法时,会指定在对象反序列化后立即调用此方法;
4) OnDeserializing:应用于某方法时,会指定在对象反序列化时调用此方法;
5) OnSerialized:如果将对象图应用于某方法,则应指定在序列化该对象图后是否调用该方法;
6) OnSerializing:当他应用于某个方法时,会指定在对象序列化前调用此方法;
1、异步与多线程。异步与多线程两者度可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。很多时候,我们分不清异步与多线程的区别,经常经他们混为一谈,其实,他们还是有区别的:
1) 异步操作本质:所有的程序最终都会由计算机硬件来执行,拥有DMA功能的硬件在和内存进行数据交互的时候可以不消耗CPU资源,这些不消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS这样的单进程系统中也同样可以发起异步的DMA操作。优点:异步操作无需额外线程负担,并且使用了回调的方式进行处理,在设计良好的情况下,处理函数可尽可能减少共享变量的使用,减少了死锁发生的可能性。缺点:异步操作编写复杂,回调难以调试。
2) 线程本质:线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能。线程本质上是进程总一段并发执行的代码,所以线程需要操作系统投入CPU资源来运行和调度。优点:编写简单。缺点:线程使用会消耗额外切换带来的负担,并且线程间共享变量可能造成死锁。
3) 适用范围:多线程适用于计算密集型工作,异步机制适用于IO密集型工作,详细参考图1。
图1 单线程、多线程适用条件
一个使用了异步操作的WinForm程序示例如下所示,点击按钮,异步获取网页源码并显示在窗体的文本控件textBox1上。
private void button1_Click(object sender, EventArgs e)
{
//开辟一个线程
Thread t = new Thread(()=>{
var request = HttpWebRequest.Create("http://www.cnblogs.com/hanganglin");
//发起异步请求
request.BeginGetResponse(this.AsyncCallbackImpl, request);
});
t.Start();
} //回调方法
public void AsyncCallbackImpl(IAsyncResult ar) {
WebRequest request = ar.AsyncState as WebRequest;
var response = request.EndGetResponse(ar);
var stream = response.GetResponseStream();
using (StreamReader reader = new StreamReader(stream)) {
var content = reader.ReadToEnd();
//由于textBox1控件是主线程创建的,在其他线程中需要调用必须采用异步机制
//如果InvokeRequired为True,则必须通过异步来修改,否则可直接修改
if (textBox1.InvokeRequired) {
textBox1.BeginInvoke(new Action(() => {
textBox1.Text = content;
}));
}
else {
textBox1.Text = content;
}
}
}
值得注意的是,创建控件线程以外的线程想访问控件,可通过控件的BeginInvoke异步方法,BeginInvoke方法是将消息发送到消息队列中等待UI所在的线程进行处理,代码:if(textBox1.InvokeRequired){ textBox.BeginInvoke(new Action(()=>{ textBox.Text = content; })); } else { textBox1.Text = content; }
1、声明变量时考虑最大值,关键字check可检查运算是否溢出,运算溢出则抛出异常。 代码:check{ ... }。
2、文件MD5哈希值判断文件内容是否修改。对文件求MD5哈希值,当文件内容被修改后再求MD5哈希值,比较两个值可判断文件内容是否被修改过。
//获取文件的md5哈希值
public static String GetFileMd5Hash(String filePath) {
using(MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
using(FileStream fs = new FileStream(filePath,FileMode.Open,FileAccess.Read,FileShare.Read)){
return BitConverter.ToString(md5.ComputeHash(fs)).Replace("-", "");
}
}
3、合适选择使用对称加密和非对称加密。对称加密加密和解密时使用了相同的密钥和加密算法,其优点是加密解密速度快,常用于大量数据传输,缺点是传输数据时需要传输密钥, 安全系数不高。非对称加密使用了不同的密钥,公钥PK和私钥SK,用公钥PK进行加密,只有用对应的私钥SK才可以解密,优点是传输加密信息时不需要传输私钥,安全系数高,缺点是算法复杂,加密解密速度很慢。
C#下的一个文件对称加密示例MySymmetricAlgorithm:
public class MySymmetricAlgorithm
{
//缓冲区大小
static int bufferSize = * ;
//密钥salt 防止“字典攻击”
static byte[] salt = { , , , , , , , , , , , , , , , };
//初始化向量
static byte[] iv = { , , , , , , , , , , , , , , , }; //初始化并返回对称加密算法
static SymmetricAlgorithm CreateRijndael(string password, byte[] salt)
{
PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, salt, "SHA256", );
SymmetricAlgorithm sma = Rijndael.Create();
sma.KeySize = ;
sma.Key = pdb.GetBytes();
sma.Padding = PaddingMode.PKCS7;
return sma;
} public static void EncryptFile(string inFile, string outFile, string password)
{
using (FileStream inFileStream = File.OpenRead(inFile), outFileStream = File.Open(outFile, FileMode.OpenOrCreate))
using (SymmetricAlgorithm algorithm = CreateRijndael(password, salt)) {
algorithm.IV = iv;
using (CryptoStream cryptoStream = new CryptoStream(outFileStream, algorithm.CreateEncryptor(), CryptoStreamMode.Write)) {
byte[] bytes = new byte[bufferSize];
int readSize = -;
while ((readSize = inFileStream.Read(bytes, , bytes.Length)) != ) {
cryptoStream.Write(bytes, , readSize);
}
cryptoStream.Flush();
}
}
} public static void DecryptFile(string inFile, string outFile, string password)
{
using (FileStream inFileStream = File.OpenRead(inFile), outFileStream = File.OpenWrite(outFile))
using (SymmetricAlgorithm algorithm = CreateRijndael(password, salt)) {
algorithm.IV = iv;
using (CryptoStream cryptoStream = new CryptoStream(inFileStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read)) {
byte[] bytes = new byte[bufferSize];
int readSize = -;
int numReads = (int)(inFileStream.Length / bufferSize);
int slack = (int)(inFileStream.Length % bufferSize);
for (int i = ; i < numReads; ++i) {
readSize = cryptoStream.Read(bytes, , bytes.Length);
outFileStream.Write(bytes, , readSize);
}
if (slack > ) {
readSize = cryptoStream.Read(bytes, , (int)slack);
outFileStream.Write(bytes, , readSize);
}
outFileStream.Flush();
}
}
}
1、区分接口和抽象类的应用场合。接口与抽象类的区别:① 接口支持多继承,抽象类只能但继承; ② 接口可以包含方法、属性、索引器、事件的签名,但不能有实现,抽象类则可以通过虚方法来实现; ③ 接口新增方法后,所有继承者必须重构,否则编译不通过,而抽象类新增虚方法后不需要(新增抽象方法也需重构)。由于存在这些区别,接口一旦被设计出来,就应该是不变的,而抽象类可以随着版本的升级增加一些功能。接口与抽象类的应用场景简单可概括为:① 如果对象存在若干功能相近且关系紧密的版本,则使用抽象类; ② 如果对象关系不紧密,但是若干功能拥有共同的声明,则使用接口; ③ 抽象类适合于提供丰富功能的场合,接口则更倾向于提供单一的一组功能。
2、优先考虑组合(Has a),然后考虑继承(Is a)。组合是将其他类型的对象作为本类型的成员使用,而继承是子类继承父类并使用。组合好比"黑盒式代码使用",继承好比"白盒式代码使用"。组合的耦合性比继承更低,封装性比继承更高。
3、开闭原则。开闭原则是面向对象设计中最重要的原则之一,是可复用设计的基石。开闭原则原话翻译:软件实体应该对扩展开放,对修改关闭。通俗地说,在软件体系扩展新功能时,不应该修改现有的代码。
1、命名术语:
1) PascalCasing,帕斯卡命名法(首字母大写),公开元素建议使用帕斯卡命名法。建议用于命名空间、类型、接口、方法、属性、事件、静态字段和枚举值。
2) camelCasing,驼峰命名法(首字母小写),非公开元素建议使用驼峰命名法。建议用于参数、私有字段和方法内变量。
2、命名规范:
1) 命名空间:使用Java中的域名域名命名法,或使用公司名作为前缀,产品名称作为第二层,其他特性作为第三层,如:PanChina.Oa.System。
2) 类型:使用名词或名词词组进行命名(如UserManager要优于UserManage),不要在类型名前加前缀,派生类名称以基类名称结尾(如Exception所有派生类都以Exception结尾)。
3) 接口:使用大写字母"I"为前缀,用形容词命名,如IDisposable表示类型可以被释放。
4) 泛型:使用大写字母"T"为前缀,多个参数使用标号,如T1、T2。
5) 枚举:枚举类型用复数命名,不要添加如"Enum"或"Flag"等后缀,枚举元素用单数命名。如enum Week { Monday,Tuesdat,.. }
6) 字段:共有字段使用帕斯卡命名法,私有字段使用驼峰命名法。使用名词或名词词组命名,不添加前缀。
7) 方法:使用动词词组命名,根据方法对应的任务命名,而不是根据内部实现细节来命名。常用动词:Get、Update、Delete、Add、Validate、Select、Search等,动词后加上动作内容,就是一个规范的方法名。
8) 属性:用名词或名词词组命名,要用肯定性的短语,如CanSeek,而不是否定短语CantSeek。当属性对应一个类型时,建议则接用类型命名属性名,如:public Company Company{ get;set; },不建议为属性制定另外名字,如TheCompany。
9) 事件:用动词或动词词组命名(如Cheked、Updated、Selected等词组),以"EventArgs"后缀结尾(绑定事件的方法名加上On)。如:UpdatedEventArgs,绑定事件方法OnUpdated()。
10) 索引器:固定设计,使用this关键字,如:public String this[int index] { get {return "";} }。
3、有条件地使用前缀,在.NET设计规范中,不建议使用前缀,如果确有特殊使用需求,建议:① 前缀m_,表示这是一个实例变量 ② 前缀s_,表示这是一个静态变量。