@TOC


前言

搞编程的,在工作中学习中,除了培养好的代码习惯、养成自己的一套代码风格,还需要多思考如何把代码写的更健壮,碰到bug如何去调试呢,与诸君共勉。

一、写代码之前:

1.属性:原本为了防止成员age,设置为公共的之后出现age = -10这种,所以设置GetXxx/SetXxx访问器进行封装,对数据通过两个public函数GetXxx/SetXxx进行合法性检查。但是咱们平时访问age属性,就是一个变量呀,用函数去访问不太符合习惯,所以C#出来一种特殊语法,属性

  • set访问器有个特殊的隐式参数value
  • 属性运行方式和函数类似,算是一种特殊函数。但是属性的使用方式还是对象或者类,和普通变量一样
  • 属性和他对应的变量同名,但是属性首字母大写Age,变量还是小写age,属性名字可以随便,但是乱起不好识别

2.编译遇到:两个输出文件名解析为同一个输出路径:“obj\x86\Debug\xxxxxx.resources”:使用小乌龟对之前的修改进行删除,不知道是增加了什么

3..NET约定,所有特性的名称都以Attribute结尾,使用时可以省略Attribute

4.Visual Studio的常用快捷键:https://learn.microsoft.com/zh-cn/visualstudio/ide/default-keyboard-shortcuts-in-visual-studio?view=vs-2022

5.适当使用#region…#endregion,可以让代码看着更简洁

二、写代码时:

1.属性和他对应的变量同名,但是属性首字母大写Age,变量还是小写age,属性名字可以随便,但是乱起不好识别

2.所有关于文件、文件流的操作都放到try-catch结构中,关于流的操作应该放在finally块中,确保无论操作成功还是失败,流都被及时释放。我们自己编写一个函数把异常处理和关闭文件的代码封装进去

  • 当一个程序读写文件时,OS通常会阻止其他程序读写这个文件。所以使用完毕后腰及时关闭文件流,否则会出现同步问题
  • 关闭方式:
    • 可以通过finally块中代码关闭
    • 可以通过C#的using语句,当退出using语句时,系统会自动关闭流对象,using适用于需要及时释放资源的代码
  • fileStream对象的声明代码要放在try语句之前,不然catch块和finally块中会出现fileStream对象的作用域问题
  • 文件(流)总结:封装自己的工具类、关闭等注意事项【有两个注意事项】,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// 处理文件的通用函数UniversalFileProcess(),只要传入文件路径和处理文件的具体代码,这个方法就会自动完成异常处理和关闭文件的操作
/// </summary>
/// <param name="path"></param>
/// <param name="doSmoething" ></param> 把处理文件的具体代码封装在doSomething()函数中
public static void UniversalFileProcess(string path, MyFileProcessCode doSomething)
{
FileStream fileStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);

try
{
//...处理文件的具体代码
doSomething(fileStream);
}
catch (Exception e)
{
//...异常处理
Console.WriteLine(e.Message);
}
finally
{
//...关闭流
if(fileStream != null)
{
//Close()方法就是通过调用Dispose()方法实现的
fileStream.Close();
}
}
}

//但是想要以函数为参数必须使用委托机制。
//定义一个名为MyFileProcessCode(FileStream file)的委托。
public delegate void MyFileProcessCode(FileStream file);

//处理文件的具体代码
public static void DoSomething(FileStream fileStream){
    byte[] datas = {100, 101, 102, 103, 104, 105};
    fileStream.Write(datas, 0, datas.Length);
}

//使用工具类的例子
public static void Main(string[] args){
    //今后咱们处理文件时,只需要把处理文件的具体代码写在一个函数中,然后调用UniversalFileProcess()函数,即可使用
    //而不用关心异常处理和关闭文件的操作
    UniversalFileProcess(@"C:/a.txt", new MyFileProcessCode(DoSomething));
}

  • 第一个注意事项:
1
2
3
4
5
6
7
8
9
10
//继续优化,可以通过匿名函数省区每次都要编写DoSomeThing()函数这一步
//下面的代码从delegate(FileStream fileStream))到最后是一个匿名函数,封装了处理文件的具体代码
static void Main(string[] args)
{
    UniversalFileProcess(@"C:/a.txt", delegate(FileStream fileStream))
    {
        byte[] datas = {100, 101, 102, 103, 104, 105};
    fileStream.Write(datas, 0, datas.Length);
    }
}
  • 第二个注意事项:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//除了用finally,还可以用using关闭文件流
//在using后的括号中创建了一个流对象fileStream,然后using的语句块中使用流对象fileStream,当退出using时系统会自动关闭流对象fileStream
using(FileStream fileStream = File.OpenText[path])
    {
        byte[] datas = {100, 101, 102, 103, 104, 105};
    fileStream.Write(datas, 0, datas.Length);
    }
}

//using的一般使用形式是:在using后的括号中创建了一个对象obj,然后using的语句块中使用对象obj,当退出using时系统会自动关闭对象obj
using(type onj = initialization)
{
    //具体代码
}
//using等价写法
{
    type obj = initialization;
    try
    {
        //具体处理代码
    }
    finally
    {
        if(obj != null)
        {
            //调用obj对象的Dispose()方法销毁对象释放资源
            ((IDisposable)obj).Dispose();
        }
    }
}

3.一个生产问题复现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
**DrawGraphThread** 是 null
System.NullReferenceException
HResult=0x80004003
Message=未将对象引用设置到对象的实例。
Source=WindowsFormsApp1
StackTrace:
at WindowsFormsApp1.Form1.suspendThreadButton_Click(Object sender, EventArgs e) in D:\DevelopData\VSExercise\暂时用,用完删除\WindowsFormsApp1\Form1.cs:line 99
at System.Windows.Forms.Control.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.ButtonBase.WndProc(Message& m)
at System.Windows.Forms.Button.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr dwComponentID, Int32 reason, Int32 pvLoopData)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.Run(Form mainForm)
at WindowsFormsApp1.Program.Main() in D:\DevelopData\VSExercise\暂时用,用完删除\WindowsFormsApp1\Program.cs:line 20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//错误代码:
/// <summary>
/// 声明工作线程
/// </summary>
Thread DrawGraphThread;

/// <summary>
/// 开始按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void startThreadButton_Click(object sender, EventArgs e)
{
//第二步:创建入口委托
ThreadStart entryPoint = new ThreadStart(DrawGraph);

//第三步:创建线程,并启动线程
new Thread(entryPoint).Start();
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//修正后的正确运行代码:
/// <summary>
/// 声明工作线程
/// </summary>
Thread DrawGraphThread;

/// <summary>
/// 开始按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void startThreadButton_Click(object sender, EventArgs e)
{
//第二步:创建入口委托
ThreadStart entryPoint = new ThreadStart(DrawGraph);

//第三步:创建线程
DrawGraphThread = new Thread(entryPoint);

//启动线程
DrawGraphThread.Start();
......
}

三、写代码后【C#软件系统性能分析的点【VS性能分析步骤:调试debug->性能探查器->勾选CPU Usage、Memory Usage等选项进行对比查看,微软的性能分析工具及方法https://learn.microsoft.com/zh-cn/visualstudio/profiling/profiling-feature-tour?view=vs-2019】】

1.垃圾回收相关想法

  • 垃圾回收器、垃圾回收算法、标记清除、标记整理、复制
    • 资源指的是一些类似于Windows句柄,数据库连接的非内存的一些本地资源,这些资源无法被运行时本身管理,需要程序员自己去申请和释放。当一些托管类型中对这些资源进行一定的封装的时候,我们就需要妥善的去处理他们,比如说在不用的时候释放到这些资源以免造成资源泄露,怎么知道什么时候不用呢?只有使用这个类型的人知道,所以我们需要在Finalize方法中对资源进行释放。Finalize方法的垃圾对象会在垃圾回收的时候放入一个“垃圾队列”,但是这个垃圾队列并不是说下一次垃圾回收的时候就会调用,而是会在下一次内存压缩(compact)的时候才会去调用,你不知道这货啥时候才会去调用那个Finalize方法,也就是说很可能你这个对象会存活很久(占用内存),并且这个资源一直得不到释放(占用资源)
  • 对集合预先分配大小
  • 使用StringBuilder
  • 值类型代替引用类型,比如用struct代替class
  • 避免Finalizer,使用Dispose Pattern
    • Finalize方法的垃圾对象会在垃圾回收的时候放入一个“垃圾队列”,但是这个垃圾队列并不是说下一次垃圾回收的时候就会调用,而是会在下一次内存压缩(compact)的时候才会去调用,你不知道这货啥时候才会去调用那个Finalize方法,也就是说很可能你这个对象会存活很久(占用内存),并且这个资源一直得不到释放(占用资源)
    • Dispose模式,实现IDispose方法来提供一个Dispose方法允许调用者手动去调用Dispose方法释放资源
    • 在以下这几种情况中我们应该为一个类型实现Dispose模式(非完全列表哦)
        1. 类型对一些本地资源进行封装,比如类型暴露了一些文件操作的接口,并在内部对文件进行了操作。
        1. 如果类型内部有一个Dispose成员,你需要保证这个成员必须在你的对象的生命周期内得到释放。比如说我实现了一个记日志的类,类的内部有一个成员变量是FileStream,我就需要实现Dispose模式来保证这个FileStream能够被释放。
        1. 如果你要实现一个基类,基类本身没有本地资源。但是他的子类有那么必须要实现Dispose模式,典型例子就是Stream
  • 缓存、使用泛型
  • 避免内存泄漏
    • 内存泄漏:闭包Closure【作用域在lambda块里的叫做闭包,lambda执行完毕这些变量会被gc销毁】。lambda 语句块里面的东西自产自销满足闭包,你全局的东西不要扔到lambda里,人家lambda表达式自产自销,你放个全局变量_id之后,就不能自产自销了图片可能打不开,可以去我的CSDN看看!
    • 内存泄漏:事件Event。
    • 内存泄漏:静态static。
      1
      2
      3
      4
      5
      6
      7
      8
      public class Static
      {
      static List<Static> _instances = new List<Static>();
      public Static()
      {
      _instances.Add(this);
      }
      }
    • 内存泄漏:非托管对象。图片可能打不开,可以去我的CSDN看看!
    • 内存泄漏:Dispose Pattern(SafeHandler)
    • 内存泄漏:Call Dispose
  • 减少大量临时托管对象被频繁创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//比如之前项目中的工厂模式,只new一次,你用你去拿就行。而不是用一次就要new一次
static class StepFactory
{
/// <summary>
/// 获取指定步骤的实例
/// </summary>
/// <param name="stepIndex"></param>
/// <returns></returns>
public static IStep GetStep(int stepIndex)
{
//如果步骤字典中包含指定的步骤索引,则返回对应的步骤实例,否则返回null
return _StepMap.ContainsKey(stepIndex) ? _StepMap[stepIndex] : null;

}

/// <summary>
/// 步骤字典,用于存储步骤索引和对应的步骤实例。字典用于存储键值对的集合,通过键快速查找对应的值
/// </summary>
private static readonly Dictionary<int, IStep> _StepMap = new Dictionary<int, IStep>()
{
{1,new MyStep1() },//使用步骤索引1创建MyStep1实例并添加到字典中
{2,new MyStep2() },//使用步骤索引2创建MyStep2实例并添加到字典中
{3,new MyStep3() },//使用步骤索引3创建MyStep3实例并添加到字典中

};
}

2.反射

  • 很容易耗费性能或出现故障,一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Program
{
static void Main(string[] args)
{
//var beginTime = System.DateTime.Now;
//Console.WriteLine();
List<MyLearnReflectClass> myLearnReflectClassList = Enumerable.Repeat(new MyLearnReflectClass(), 10000000).ToList();
object aux = 0;

//foreach (var obj in myLearnReflectClassList)
//{
// aux = obj.Number;
// obj.Number = 3;
//}
//var endTime = System.DateTime.Now;
//Console.WriteLine(endTime - beginTime);//00:00:00.3718429
//Console.ReadLine();

var beginTime2 = System.DateTime.Now;
System.Reflection.PropertyInfo propertyInfo = typeof(MyLearnReflectClass).GetProperty("Number");

foreach (var obj in myLearnReflectClassList)
{
aux = propertyInfo.GetValue(obj);
propertyInfo.SetValue(obj, 3);
}
var endTime2 = System.DateTime.Now;
Console.WriteLine(endTime2 - beginTime2);//00:00:02.2009012
Console.ReadLine();
}
}

public class MyLearnReflectClass
{
public int Number { get; set; }
}
...

3.数据结构

  • 数据结构可以用常见的时间复杂度来衡量,C#中常见的字典、集合、列表的时间复杂度
    • List 列表是顺序线性表,Add 操作是 O (1) 或 O (N),因为 List 是动态扩容的,在未扩容之前,其 Add 操作是 O (1),而在扩容的时候,Add 操作是 O (N) 的。其 Contains 方法,是按照线性检索的,其复杂度是 O (n)
    • SortedList 列表是有序线性表,Add 操作是 O(n), 其 Contains 方法是通过二分查找检索元素的,因此复杂度是 O(lg n),其 Containskey 方法也是通过二分查找检索元素,复杂度也是 O(lg n),ContainsValue 方法是使用线性查找元素,复杂度是 O(n)
    • HashSet 集合类是包含不重复项的无序 hash 表 (非线性),它本身是一个一维数组,但是二维链表结构 (扩展:一维数组的大小总是 2 的 N 次方)。Add 操作是 O(1)或是 O(N)的,原因同 List 集合类。Contains 方法是 O(1)
    • SortedSet 集合类是基于红黑树实现的,其 Add 方法是 O(lg n),Contains 方法也是 O(lg n)
    • Dictionary 字典类是 hash 表,Add 操作是 O(1)或是 O(N)的。其 Containskey 方法是 O(1),原因是通过 hash 来查找元素而不是遍历元素。ContainsValue 方法的时间复杂度是 O(N),原因是内部通过遍历 key 来查找 value,而不是通过 hash 来查找。Item [Key] 属性根据 key 来检索 value,其时间复杂度也是 O (1)
    • SortedDictionary 字典类是基于平衡二叉树实现的,其 Add 方法是 O(lg n),ContainsKey 方法也是 O(lg n),而 ContainsValue 方法则是 O(n)
  • 使用合适的数据结构
  • 局部性原理
    • 时间局部性
      • 如果某个变量或者某句代码这次被访问,那么有可能在不久的将来会被再次访问,这种情况下我们可以把常用的数据加cache来优化访问
      • 如果某个位置的信息被访问,那和他空间上相邻的信息也很有可能被访问,因为大部分时候我咱们代码都是顺序执行,数据也是顺序访问的
    • 空间局部性
    • 具体情况具体分析,时间换空间,空间换时间

4.其他情况,如同步异步、使用不同算法、拆箱装箱【值类型、引用类型】、Try Catch、Dynamic、Lock、互操性、AddRange替换Add+ forngen.exe与编译


巨人的肩膀

  • 周志明老师的凤凰架构
  • C#高级编程
  • C#函数式编程
  • 叩响C#之门
  • 组内各位前辈们的指导