0613学习 agile Posted on Jun 20 2023 面试 unity基础 1.c#闭包 闭包(Closure)指的是一个函数(或委托)以及它所引用的外部变量的组合。闭包允许函数访问其外部范围内的变量,即使函数在该范围之外被调用时也是如此。 --- 闭包可以导致内存泄漏的问题。如果闭包引用了一个大型对象,而该对象在闭包之后不再需要,但闭包仍然存在,那么该对象可能不会被垃圾回收。因此,在使用闭包时,应谨慎管理内存和生命周期 --- 闭包会延长它使用的外部变量的生命周期,直到闭包本身被释放 --- 闭包在以下情况下可能会导致垃圾回收(Garbage Collection,GC)的问题: - 循环引用:如果闭包中引用了外部范围的对象,并且这些对象也引用了闭包,形成了循环引用,那么这些对象可能不会被垃圾回收。这是因为垃圾回收器需要确定对象是否可达,而循环引用会导致这种判断变得困难。为了避免这个问题,可以在不再需要闭包时手动解除引用,或者使用弱引用(WeakReference)来引用外部对象 ```C# using System; class Program { static void Main() { Action outer = null; Action inner = null; outer = () => { Console.WriteLine("Outer"); inner = () => { Console.WriteLine("Inner"); outer(); // 在内部函数中调用外部函数 }; }; outer(); // 调用外部函数 // 手动解除对闭包的引用 outer = null; inner = null; // 垃圾回收器可能无法释放内存,因为存在循环引用 // 这会导致 "Outer" 和 "Inner" 不断打印,程序不会结束 } } ``` - 长时间存活的闭包:如果闭包存活时间很长,而其中引用的对象占用了大量内存,那么这些对象可能会被保留在堆上,即使它们已经不再需要。这可能导致内存泄漏。为了解决这个问题,可以手动解除对闭包的引用,或者将闭包转换为短期的委托,以便在使用后释放内存。 --- ```C# //for循环问题,执行大时候打出来的一直为5 // for (cDisplayClass00.i = 0; cDisplayClass00.i < 5; cDisplayClass00.i++) // l.Add(new Func((object) cDisplayClass00, __methodptr(<test>b__0))); //可以看到i在底层,是作为cDisplayClass00的属性 //而在循环中只定义了一次cDisplayClass00,循环结束后 //cDisplayClass00.i也就变成5了 for (int i = 0; i < 5; i++) { l.Add(() => { Console.WriteLine(i); }); } ``` --- ```C# //跟上诉区别是在闭包中没有直接引用i,而是引用了j //for (int i = 0; i < 5; ++i) //{ // TestFor.<>c__DisplayClass0_0 cDisplayClass00 = new TestFor.<>c__DisplayClass0_0(); // cDisplayClass00.j = i; // l.Add(new Func((object) cDisplayClass00, __methodptr(<test>b__0))); //} //c__DisplayClass0_0类定义了5次 for (int i = 0; i < 5; i++) { int j = i; l.Add(() => { Console.WriteLine(j); }); } ``` --- ```C# //int[] numArray = new int[5]{ 0, 1, 2, 3, 4 }; //for (int index = 0; index < numArray.Length; ++index) //{ // TestForeach.<>c__DisplayClass0_0 cDisplayClass00 = new TestForeach.<>c__DisplayClass0_0(); // cDisplayClass00.i = numArray[index]; // l.Add(new Func((object) cDisplayClass00, __methodptr(<test>b__0))); //} int[] a = { 0, 1, 2, 3, 4 }; foreach (int i in a) { l.Add(() => { Console.WriteLine(i); }); } ``` --- ```C# using System; class Program { static void Main() { Action longLivedClosure = null; void CreateClosure() { string data = "Long-lived data"; longLivedClosure = () => { Console.WriteLine(data); }; } CreateClosure(); // 创建闭包 // 手动解除对闭包的引用 longLivedClosure = null; // 此时,闭包中的 data 对象仍然存在于内存中,造成内存泄漏 } } ``` --- 2.中缀转后缀表达式 - 后缀计算:一个栈,遍历字符串,遇到数字,压栈,遇到运算符,栈弹两个计算再压栈。 --- ```C# public class Solution { public int EvalRPN(string[] tokens) { var stack = new Stack<int>(); int left; int right; foreach (string token in tokens){ switch(token){ case "+" : right = stack.Pop(); left = stack.Pop(); stack.Push(left + right); break; case "-" : right = stack.Pop(); left = stack.Pop(); stack.Push(left - right); break; case "*" : right = stack.Pop(); left = stack.Pop(); stack.Push(left * right); break; case "/" : right = stack.Pop(); left = stack.Pop(); stack.Push(left / right); break; default: stack.Push(int.Parse(token)); break; } } return stack.Pop(); } } ``` - 中缀转后缀求值:是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。 我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。 如果比运算符栈顶元素的优先级高,就将当前运算符压入栈; 如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。 `(` 的优先级最低为0 --- 由于乘除优先于加减计算,因此不妨考虑先进行所有乘除运算,并将这些乘除运算后的整数值放回原表达式的相应位置,则随后整个表达式的值,就等于一系列整数加减后的值。 基于此,我们可以用一个栈,保存这些(进行乘除运算后的)整数的值。对于加减号后的数字,将其直接压入栈中;对于乘除号后的数字,可以直接与栈顶元素计算,并替换栈顶元素为计算后的结果。 具体来说,遍历字符串 sss,并用变量 preSign 记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据 preSign 来决定计算方式: 加号:将数字压入栈; 减号:将数字的相反数压入栈; 乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。 代码实现中,若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾。处理完该数字后,更新 preSign 为当前遍历的字符。 遍历完字符串 sss 后,将栈中元素累加,即为该字符串表达式的值。 ```C# public class Solution { public int Calculate(string s) { Stack<int> stack = new Stack<int>(); char preSign = '+'; int num = 0; int n = s.Length; for (int i = 0; i < n; ++i) { if (char.IsDigit(s[i])) { num = num * 10 + s[i] - '0'; } if (!char.IsDigit(s[i]) && s[i] != ' ' || i == n - 1) { switch (preSign) { case '+': stack.Push(num); break; case '-': stack.Push(-num); break; case '*': stack.Push(stack.Pop() * num); break; default: stack.Push(stack.Pop() / num); break; } preSign = s[i]; num = 0; } } int ans = 0; while (stack.Count > 0) { ans += stack.Pop(); } return ans; } } ``` --- 3.C#中unsafe用法 --- 当一个代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用指针变量。不安全代码或非托管代码是指使用了指针变量的代码块 - 指针声明和使用: ```C# unsafe { int x = 10; int* p = &x; Console.WriteLine(*p); // 输出:10 *p = 20; Console.WriteLine(x); // 输出:20 } ``` 在`unsafe`代码块中,可以声明指针类型变量,并使用&运算符获取变量的地址,*运算符用于访问指针指向的值 --- - 指针算术运算: ```C# unsafe { int[] numbers = { 1, 2, 3, 4, 5 }; fixed (int* p = numbers) { int* p2 = p; Console.WriteLine(*p2); // 输出:1 Console.WriteLine(*(p2+2)); // 输出:3 } } ``` 使用指针算术运算可以实现对数组的指针访问,可以通过对指针进行加法或减法运算来访问不同的数组元素。 --- - 使用`stackalloc`分配栈上内存: ```C# unsafe { int* p = stackalloc int[3]; p[0] = 1; p[1] = 2; p[2] = 3; Console.WriteLine(p[1]); // 输出:2 } ``` `stackalloc`关键字用于在栈上分配一块内存,可以使用指针来操作该内存。 --- - 调用非托管代码: ```C# unsafe { byte[] buffer = new byte[1024]; fixed (byte* p = buffer) { // 调用非托管函数 ProcessData(p, buffer.Length); } } ``` `fixed`关键字用于固定托管数组,以便在`unsafe`代码块中获取数组的指针,然后可以将指针传递给非托管函数进行处理 --- - `StructLayout`是一个特性类,用于控制结构体的内存布局和对齐方式。它可以应用于结构体类型,以显式地指定结构体在内存中的排列方式。 StructLayout特性具有以下两个常用的参数: LayoutKind:用于指定结构体的排列方式,可选值有: Sequential:按照字段的声明顺序在内存中紧凑排列。 Explicit:字段按照用户显式指定的偏移量排列。 Auto:由运行时环境自动选择最合适的排列方式。这是默认值。 Pack:用于指定结构体的最小对齐字节数。默认情况下,结构体的对齐方式根据平台的规则确定。Pack参数的取值范围为1到255。 --- 默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同,即按照结构中占用空间最大的成员进行对齐(Align); 使用LayoutKind.Explicit的情况下,CLR不对结构体进行任何内存对齐(Align),而且我们要小心就是FieldOffset; 使用LayoutKind.Auto的情况下,CLR会对结构体中的字段顺序进行调整,使实例占有尽可能少的内存,并进行4byte的内存对齐(Align) --- 空struct实例的Size ```C# struct EmptyStruct{} ``` 无论运用上面LayoutKind的Explicit、Auto还是Sequential,得到的`sizeof(EmptyStct)`都是`1byte`,即使Pack设置为4,获取的还是1。 --- - float转int ```C# // 当把float转int,可以使用(int)强制转换, // 但是这个转换不是四舍五入的,会舍去小数点后的所有 var a = 12.0f; var b = 0.300000012f; var c = a / b; c = (int)c; var d = (int)(a / b); var f = Convert.ToInt32(a / b); var g = (int)((double)a / (double)b); //不是四舍五入 var h = (int)1.6f; // 40,39,40,39,1 Debug.Log($"c:{c},d:{d},f:{f},g:{g},h:{h}"); ``` 0620学习 面试之语言篇