C# 一些代码细节规范

这篇文章的起因是因为看到很多项目在设计上和功能实现上都很高大上,但是一些细节代码却不堪入目。本文准备从代码细节上规范代码。

此文不涉及命名规范和注释规范。另外如果有不实之处还望在评论区指出。

一、禁止使用加号拼接字符串

项目中总是看到用+号去拼接字符串,但是我觉得完全可以用string.Concat()、string.Format()、StringBuilder、$、string.Join 等方式替代用加号拼接字符串。

原因:无论是从性能上还是可读性来说都比+号好一点。(常量拼接除外,因为常量用+拼接编译的时候会直接变成结果字符串。)

反例

string str = "欢迎您,";
string name = "碌云";
str += name;
Console.WriteLine(str);
string name = "碌云";
Console.WriteLine("欢迎您," + name);
string str = "欢迎您,";
string name = "碌云";
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("<span>");
stringBuilder.Append(str + name);
stringBuilder.Append("</span>");
Console.WriteLine(stringBuilder.ToString());

正例

string name = "碌云";
Console.WriteLine(string.Concat("欢迎您,", name));
string name = "碌云";
Console.WriteLine($"欢迎您,{name}");
string str = "欢迎您,";
string name = "碌云";
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("<span>");
stringBuilder.AppendFormat("{0}{1}", str, name);
stringBuilder.Append("</span>");
Console.WriteLine(stringBuilder.ToString());

二、能用string.Empty的地方禁止用""

原因:可读性更好,性能也更好。(性能可能是一样的,性能这点争议性比较大。)

反例:

public void GetUserInfoById(string id)
{
  if (id == "")
  {
    throw new Exception("ID不能为空");
  }
}
string str = "aaaaaaaa11111111aaaaa";
Console.WriteLine(str.Replace("1", ""));

正例:

public void GetUserInfoById(string id)
{
//一般看业务来的,实际开发中可能是用string.IsNullOrEmpty,这里只是举例原代码为""时可以改成这样子。
if (id == string.Empty) {
throw new Exception("ID不能为空");
}
}
string str = "aaaaaaaa11111111aaaaa";
Console.WriteLine(str.Replace("1", string.Empty));

三、继承自IDisposable的非静态类一定要释放,能用using释放就必须用using

为什么是非静态类呢...因为我之前用.netcore发邮件的类也给它释放了,结果第二次发不了邮件了,所以静态的类还是不要释放了。。

原因:非托管资源必须手动释放,using做了优化:哪怕内部报错了还是会释放 。(C#8.0开始可以直接在语句前写using,点击这里查看

反例:

/// <summary>
/// MD5加密
/// </summary>
public string MD5Encrypt(string plainText)
{
return BitConverter.ToString(new MD5CryptoServiceProvider().ComputeHash(Encoding.Default.GetBytes(plainText))).Replace("-", string.Empty);
}

正例:

/// <summary>
/// MD5加密
/// </summary>
public string MD5Encrypt(string plainText)
{
  using (var md5c = new MD5CryptoServiceProvider())
  {
    return BitConverter.ToString(md5c.ComputeHash(Encoding.Default.GetBytes(plainText))).Replace("-", string.Empty);
  }
}

四、禁止嵌套使用using释放资源

可以使用连续using或者利用IDisposable中介的方式释放资源

原因:编译后会生成多个try影响性能并且代码也不美观

反例:

private DataSet GetDataSet(string cmdText, CommandType cmdType, params SqlParameter[] parameters)
{
  if (string.IsNullOrEmpty(cmdText)) return null;

  using (SqlConnection conn = GetSqlConnection())
  {
    using (SqlCommand comm = new SqlCommand())
    {
      using (SqlDataAdapter adap = new SqlDataAdapter(comm))
      {
        PrepareCommand(conn, comm, cmdText, cmdType, null, parameters);
        DataSet data = new DataSet();
        adap.Fill(data);
        return data;
      }
    }
  }
}

正例:

/// <summary>
/// 执行命令返回DataSet
/// </summary>
/// <param name="cmdText">要执行的命令</param>
/// <param name="cmdType">命令类型</param>
/// <param name="parameters">参数</param>
/// <returns></returns>
private DataSet GetDataSet(string cmdText, CommandType cmdType, params SqlParameter[] parameters)
{
    //如果传入进来的sql是空的则直接return
    if (string.IsNullOrEmpty(cmdText)) return null;

    //创建数据库连接对象
    using (SqlConnection conn = GetSqlConnection())
    //创建数据命令对象
    using (SqlCommand comm = new SqlCommand())
    //获取SqlDataAdapter
    using (SqlDataAdapter adap = new SqlDataAdapter(comm))
    {
        //打开数据库连接并初始化命令对象
        PrepareCommand(conn, comm, cmdText, cmdType, null, parameters);
        //返回的data
        DataSet data = new DataSet();
        adap.Fill(data);
        return data;
    }
}
private DataSet GetDataSet(string cmdText, CommandType cmdType, params SqlParameter[] parameters)
{
    if (string.IsNullOrEmpty(cmdText)) return null;

    SqlConnection conn = GetSqlConnection();
    SqlCommand comm = new SqlCommand();
    SqlDataAdapter adap = new SqlDataAdapter(comm);
    using (IDisposable a = conn, b = comm, c = adap)
    {
        PrepareCommand(conn, comm, cmdText, cmdType, null, parameters);
        DataSet data = new DataSet();
        adap.Fill(data);
        return data;
    }
}

五、循环内的中间变量声明到循环外面

原因: 放在循环中声明代码会执行多次,放在外面只会声明一次。

说明:其实感觉放到里面问题也不大,编译器应该会自己优化,不需要我们手动的放到外面

反例:

List<MenuTreeModel> list = new List<MenuTreeModel>();
foreach (var item in paMenus)
{
    MenuTreeModel menu = new MenuTreeModel()
    {
        href = item.Url,
        icon = item.Icon,
        mid = item.Mid,
        target = item.Target,
        title = item.Name
    };
    list.Add(menu);
}

正例:

List<MenuTreeModel> list = new List<MenuTreeModel>();
MenuTreeModel menu;
foreach (var item in paMenus)
{
    menu = new MenuTreeModel()
    {
        href = item.Url,
        icon = item.Icon,
        mid = item.Mid,
        target = item.Target,
        title = item.Name
    };
    list.Add(menu);
}

六、for循环的第二段不要调用方法或者属性

原因:因为每次循环都会进入第二段所以每次访问还是有点效率问题的

反例:

List<string> list = new List<string>() { "碌云" };

for (int i = 0; i < list.Count; i++)
{
    Console.WriteLine(list[0]);
}

正例:

List<string> list = new List<string>() { "碌云" };

for (int i = 0, count = list.Count; i < count; i++)
{
    Console.WriteLine(list[0]);
}

七、用运算的方式增加代码可读性

原因:可读性高,常量运算和直接写常量性能是一样的。(运算会被编译器优化成常量,所以没有性能损耗。)

反例:

public static async Task Main(string[] args)
{
    await Task.Delay(120000);
    Console.WriteLine("等待两分钟后输出");
}

正例:

public static async Task Main(string[] args)
{
    await Task.Delay(1000 * 60 * 2);
    Console.WriteLine("等待两分钟后输出");
}

八、在需要的时候才await

原因:await越少性能就越好,微软的源码中也有很多地方是直接返回的task,而不是await后返回。这个可以详细研究下await/async的原理。

反例:

public static async Task Main(string[] args)
{
    Console.WriteLine(await GetName());
}

public static async Task<string> GetName()
{
    return await Task.FromResult("碌云");
}

正例:

public static async Task Main(string[] args)
{
    Console.WriteLine(await GetName());
}
        
public static Task<string> GetName()
{
    return Task.FromResult("碌云");
}

九、不要在循环中使用try/catch,应该把try/catch放到外层

原因:性能更好。具体原因未知,求大佬补充

值得注意的地方就是这样子会导致如果发生错误了循环就进行不下去了,这个实际中还是看业务来吧。

反例:

for (int i = 0; i < 100; i++)
{
    try
    {
        Console.WriteLine($"输出{i + 1}");
    }
    catch
    {
        Console.WriteLine("输出字符串错误");
    }
}

正例:

try
{
    for (int i = 0; i < 100; i++)
    {
        Console.WriteLine($"输出{i + 1}");
    }
}
catch
{
    Console.WriteLine("输出字符串错误");
}

十、尽量使用nameof获取名称

原因:可读性强,并且如果被引用的地方发生变更程序会报错,这样就可以一起进行相应的更改。

反例:

public class User
{
    public string Name { get; set; }
}

public User EditUser(User user)
{
    if (string.IsNullOrEmpty(user.Name))
    {
        throw new Exception("Name 不能为空");
    }
    //...省略很多代码
}

正例:

public class User
{
    public string Name { get; set; }
}

public User EditUser(User user)
{
    if (string.IsNullOrEmpty(user.Name))
    {
        throw new Exception($"{nameof(User.Name)} 不能为空");
    }
    //...省略很多代码
}

十一、使用Environment.NewLine替代直接写换行符

1.Windows 中的换行符"\r\n"

2.Unix/Linux 平台换行符是 "\n"。

3.MessageBox.Show() 的换行符为 "\n"

4.Console 的换行符为 "\n"

换行符还因平台差异而不同。

为保持平台的通用性,可以用系统默认换行符 System.Environment.NewLine。

出处:https://www.cnblogs.com/skykang/archive/2011/12/08/2281084.html

反例:

Console.WriteLine("碌云\n欢迎您");

正例:

Console.WriteLine($"碌云{Environment.NewLine}欢迎您");

十二、不要使用转换大小写的方式比较字符串相等

反例:

string str1 = "abc123";

string str2 = "ABC123";

//这种写法首先运行效率很慢,其次呢如果字符串为null有报错的风险,所以很不推荐
if (str1.ToLower() == str2.ToLower())
{
    System.Console.WriteLine("相等");
}
if (str1.ToUpper() == str2.ToUpper())
{
    System.Console.WriteLine("相等");
}

正例:

if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase))
{
    System.Console.WriteLine("相等");
}

if (string.Compare(str1, str2, StringComparison.OrdinalIgnoreCase) == 0)
{
    System.Console.WriteLine("相等");
}

暂且写到这里,如果后面想到了这篇博客还是会继续更新的。