【Unity】【C#】CSharp小技巧

标题虽然是小技巧,但本篇文章里还是记录了一些常用的功能,本文章的主要目的是方便日后快速查找


Linq

Where

Sort

时间复杂度为O(nlogn)

默认排序

1
2
int[] numbers = { 5, 2, 8, 3, 1 };
Array.Sort(numbers); // 排序后:{1, 2, 3, 5, 8}

自定义比较器

1
2
string[] names = { "Alice", "Bob", "Charlie" };
Array.Sort(names, (x, y) => y.Length.CompareTo(x.Length)); // 按字符串长度降序

多条件排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

var people = new List<Person>
{
new Person { Name = "Alice", Age = 30 },
new Person { Name = "Bob", Age = 25 },
new Person { Name = "Charlie", Age = 25 }
};

// 按年龄升序,再按姓名升序
people.Sort((x, y) =>
{
int ageComparison = x.Age.CompareTo(y.Age);
return ageComparison == 0 ? x.Name.CompareTo(y.Name) : ageComparison;
});

部分排序

1
2
3
List<int> numbers = new List<int> { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
// 仅对索引 2 到 6 进行排序
numbers.Sort(2, 5, Comparer<int>.Default); // {10, 9, 3, 4, 5, 6, 8, 7, 2, 1}

拓展:

1
public delegate int Comparison<in T>(T x, T y);

输入参数

  • xy 是要比较的两个对象,类型为 T

返回值

  • 一个整数,用来表示排序顺序:
    • 负数:表示 x 小于 yx 排在 y 前面)。
    • :表示 x 等于 y(两者顺序不变)。
    • 正数:表示 x 大于 yx 排在 y 后面)。
1
x.CompareTo(y) = 20.CompareTo(10)	// 返回1
1
2
3
4
5
6
List<int> numbers = new List<int> { 5, 2, 8, 3, 1 };

numbers.Sort((x, y) => x.CompareTo(y)); // 使用 Lambda 表达式定义 Comparison<T>

// 输出:1, 2, 3, 5, 8
numbers.ForEach(Console.WriteLine);

类型名称

  • nameof():参数的名称(编译时确定好的常量),如果是泛型参数则输出T
  • typeof().Name:类名的名称(编译时确定好的常量),如果是泛型参数则会输出泛型类实例化时的泛型类型(泛型函数的泛型参数的泛型参数)如果参数不是泛型参数,则与nameof()的效果一样
  • obj.GetType().Name:实时获取obj的类型

测试代码:

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
public class Item{}
public class ItemA : Item{}
public class ItemB : Item{}

public class Test : MonoBehaviour
{
private Test<Item> test = new();
private void Awake()
{
Debug.Log($"{nameof(Item)} {nameof(ItemA)} {nameof(ItemB)}"); // Item ItemA ItemB
Debug.Log($"{typeof(Item).Name} {typeof(ItemA).Name} {typeof(ItemB).Name}"); // 同上
test.FuncTO<Item>(); // TO Item
test.FuncTO<ItemA>(); // TO ItemA
test.FuncTO<ItemB>(); // TO ItemB
test.FuncT(new Item()); // T Item Item
test.FuncT(new ItemA()); // T Item ItemA
test.FuncT(new ItemB()); // T Item ItemB
}
}

public class Test<T>
{
public Test()
{
Debug.Log($"构造函数: {nameof(T)} {typeof(T).Name}"); // T Item
}

public void FuncTO<TO>()
{
Debug.Log($"FuncTO: {nameof(TO)} {typeof(TO).Name}");
}

public void FuncT(T item)
{
Debug.Log($"FuncT: {nameof(T)} {typeof(T).Name} {item.GetType().Name}");
}
}


链式方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ChainedClass
{
public string name;
public int age;

public ChainedClass(string name) // 通过构造函数赋值
{
this.name = name;
}

public ChainedClass SetAge(int age) // 通过链式方法赋值
{
this.age = age;
return this;
}
}
1
2
3
4
5
ChainedClass cc = new ChainedClass("coffee")		// 构造函数
.SetAge(10); // 链式方法
Console.WriteLine(cc.Age); // 输出10
cc.SetAge(20); // 再次使用链式方法
Console.WriteLine(cc.Age); // 输出20

扩展方法

1
2
3
4
public static class DotsExtensions
{
public static float3 V2ToF3(this Vector2 v2) => new float3(v2.x, v2.y, 0);
}

Operator重载运算符

重写+-*/|$!===,完全由自己定义,返回的类型甚至不需要是自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public struct CustomStruct		// 结构体或类都可以
{
public float X, Y, Z;

public static CustomStruct operator +(CustomStruct a, CustomStruct b)
{
return new CustomStruct { X = a.X + b.X, Y = a.Y + b.Y, Z = a.Z + b.Z };
}

public override string ToString()
{
return $"({X}, {Y}, {Z})";
}
}

可以像数字一样直接使用+法了

1
2
3
4
CustomStruct cs1 = new CustomStruct() { X = 1, Y = 2, Z = 3 };
CustomStruct cs2 = new CustomStruct() { X = 4, Y = 5, Z = 6 };

Console.WriteLine($"cs1 + cs2 = {cs1 + cs2}"); // 运算时自动调用`operator +`方法

implicit隐式类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public struct PlayerMoveInput
{
public float Value;

public static implicit operator PlayerMoveInput(float value)
{
return new PlayerMoveInput() { Value = value };
}

public static implicit operator float(PlayerMoveInput input)
{
return input.Value;
}
}
1
2
3
4
5
6
var input = new PlayerMoveInput();

input = 1; // 隐式转换 float -> PlayerMoveInput

// 需要注意我们这里并没有重写ToString,但是依然输出了1,一般是输出`PlayerMoveInput`
Console.WriteLine(input); // 隐式转换 PlayerMoveInput -> float

explicit显式类型转换

和隐式转换的语法一样,将implicit换成explicit就OK了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PlayerMoveInput
{
public float Value;

public static explicit operator PlayerMoveInput(float value)
{
return new PlayerMoveInput() { Value = value };
}

public static explicit operator float(PlayerMoveInput input)
{
return input.Value;
}
}
1
2
3
4
5
var input = new PlayerMoveInput();

input = (PlayerMoveInput)1; // 显示转换 float -> PlayerMoveInput

Console.WriteLine((float)input); // 输出1 显式转换 PlayerMoveInput -> float

访问权限

私有化构造函数

单例类,是不需要外界实例化的,可以将构造函数私有化,外界就无法实例化了。

1
2
3
4
5
class Manager
{
public static Manager Instance = new Manager(); // 单例类
private Manager() { } // 私有化构造函数
}

接口封装

不希望外界访问这个接口里所有的方法时,可以显示实现接口

1
2
3
4
5
6
public interface IInterface
{
void Add();
void Remove();
int Get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ReadOnlyClass : IInterface						// 创建一个只能读取,不能更改内容的类
{
void IInterface.Add() // 显示接口实现,外界无法访问
{
throw new NotSupportedException("Readonly Not Supported Add"); // 就算访问了,也会抛异常
}

void IInterface.Remove() // 显示接口实现,外界无法访问
{
throw new NotSupportedException("Readonly Not Supported Remove"); // 就算访问了,也会抛异常
}

public int Get() // 正常访问Get()方法
{
return 1;
}
}

ReadOnlyDictionary<TKey, TValue>()ReadOnlyList<>ReadOnlyCollection<>ReadOnlySet<>都是使用显示实现接口的方法隐藏了修改的方法,使外界只能读取,不能修改

完整使用方法:

1
2
3
4
5
6
7
8
9
class Manager
{
private Dictionary<string, string> _uiStack;
public ReadOnlyDictionary<string, string> UIStack { get; } // 注意这里没有使用set,完全杜绝了修改的可能
public Manager()
{
UIStack = new ReadOnlyDictionary<string, string>(_uiStack);
}
}

拓展:

{ get; private set; }:在类内部还能重新setter

{ get; }:只能在构造函数中setter


列表

自动排序列表

创建一个按照特定规则排列的列表:

例如,实例化一些Human,并且将他们按照age从大到小的方式放置一个列表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IAnimal
{
public int age { get; set; }
public string name { get; set; }
}
public class Human : IAnimal
{
public int age { get; set; }
public string name { get; set; }

public Human(int age, string name)
{
this.age = age;
this.name = name;
}
}

定义一个单例泛型类,用来做比较

1
2
3
4
5
6
public class ReverseComparer<T> : IComparer<T>
{
public static readonly ReverseComparer<T> Instance = new ReverseComparer<T>(); // 饿汉单例
private ReverseComparer() { } // 私有构造函数,不需要用户创建实例
public int Compare(T x, T y) => Comparer<T>.Default.Compare(y, x); // 实现接口,定义比较方法,注意参数换位置了
} // 如果想要从小到大排列,再换回来就好了
1
2
3
4
5
6
7
public class HumanSelector : SortedList<int, IAnimal>		// 继承`SortedList<int, IAnimal>`基类
{
public HumanSelector() : base(ReverseComparer<int>.Instance) {} // 构造函数,并将参数传递给基类的构造函数
public void Add<T>(T man) // 定义了一个泛型方法
where T : IAnimal // 这个泛型类必须是来自IAnimal接口的
=> Add(man.age, man); // 调用了基类的Add方法
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
Human A = new Human(20, "A");
Human B = new Human(25, "B");
Human C = new Human(30, "C");
HumanSelector humanSelector = new HumanSelector(); // 实例化排序列表
humanSelector.Add(C);
humanSelector.Add(A);
humanSelector.Add(B);
humanSelector.ToList().ForEach(man => { Console.WriteLine($"{man.Key}, {man.Value.name}"); });

// 30, C
// 25, B
// 20, A

这样,我们就将排序封装起来了,使用者只需要使用Add将元素添加到排序列表中就OK了

遍历列表删除符合要求的元素

  • 使用常规方法遍历列表直接删除元素会导致遍历循环顺序错误
  • 复制列表会占用内存

最快速也是最简单的方法:反向遍历,从大索引号开始遍历

1
2
3
4
5
6
7
8
List<int> list = new List<int>(){ 1, 2, 3, 4, 5 };
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] >= 4)
{
list.RemoveAt(i);
}
}

字段与属性

索引器

总得来说大致有三种用法

  1. 最普通的,针对字段复制了一个属性供外界使用,每次访问都会创建一个值类型的副本。
1
2
3
4
5
6
private int _count;							// 字段:内部使用
public int Count // 属性:外部可读可写
{
get => _count;
set => _count = value / 2; // 可自定义读写需求
}
1
2
private int _length;
public int Length => _length; // 属性:公共只读
1
public static int TotalCount { get; set; }		// 属性:可读可写,这种方式实际上也隐式的创建了一个字段,如下图

  1. 使用了ref修饰,使Run实际上是直接访问的_run这个字段,减少了一个复制的过程,提高性能
1
2
3
[SerializeField]
private bool _run; // 内部使用
public ref bool Run => ref _run; // 外部使用,这里实际上是直接访问的_run这个字段
  1. 可以让类像列表一样访问
1
2
3
4
5
public string this[int index]
{
get => _items[index];
set => _items[index] = value;
}

甚至还可以将字典改成与python一样的用法:不过不推荐这种用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CustomizeDict
{
private Dictionary<string, string> _data = new Dictionary<string, string>();
public object this[string key]
{
get => _data[key];
set
{
if (_data.ContainsKey(key))
_data[key] = value.ToString();
else
_data.Add(key, value.ToString());
UpdateContent();
}
}
}

CustomizeDict["speed"] = 500;

default

常用default能使代码排版更好看,可以省去写判断表达式

值类型

1
2
3
int number = defaule;   // 默认值为0
double amount = default// 默认值为0
bool flag = default; // 默认值为false

引用类型

1
2
string text = default;		// 默认值为null
List<int> numbers = defalut; // 默认值为null

字符串、数组切片

C#在8.0后字符串和数组可以像python一样使用切片,时代码变的即简单又美观

值得注意的是列表不能使用这个小技巧

示例:

1
2
3
4
5
6
7
8
9
10
var str = @"HelloWorld";

Console.WriteLine(str[1..5]); // "ello"

Console.WriteLine(str[^1]); // "d"
Console.WriteLine(str[..^0]); // "HelloWorld"

Console.WriteLine(str[6..^2]); // "or"
Console.WriteLine(str[^5..^1]); // "Worl"
Console.WriteLine(str[^7..8]); // "loWor"
  • 这里的^python里的-一样:反着数,^1表示最后一个字符d^5表示倒数第五个字符W
  • 左闭右开,包含第一个参数,不包含第二个参数

简写判断语句

这里面的所有简写rider都会提示,部分简写不建议使用,大大提高的代码的阅读效率

一个数介于两个数之间

1
2
3
4
5
var ran = new Random();
int i = ran.Next(20); // 在0-20之间随机选一个数

if (i is < 10 and > 0)
Console.WriteLine("i介于0和10之间");

值得注意一点的是,这个判断的两个数必须是常量值

如果是list.Count,这里就用不了了

循环判断

为方便理解我先写成这样,解释一下每个方法的含义

1
2
3
4
var list = new List<int>() { 5, 2, 7, 1, 9, 4 };		// 定义一个int列表
IEnumerable<int> temp = list.Where(i => i > 3); // 获取这个列表中大于3的元素的IEnumerable<int>枚举
list = temp.ToList(); // 将IEnumerable<int>枚举转换成列表
list.ForEach(Console.WriteLine); // 使用方法组的方式逐个打印结果

可简写成如下

1
2
3
4
5
var list = new List<int>() { 5, 2, 7, 1, 9, 4 };
foreach (var j in list.Where(i => i > 3)) // 提一嘴,里的i和j其实是分隔开的,并不会相互影响,j可以写成i
{
Console.WriteLine(j);
}

老实做法:

1
2
3
4
5
6
var list = new List<int>() { 5, 2, 7, 1, 9, 4 };
foreach (var i in list)
{
if (i > 3)
Console.WriteLine(i);
}

这一样看上面的方法是不是即简洁又好看

但其实这些都是rider编辑器会提示或者直接帮我们转的,只要别说看不懂是什么意思就行了

赋值判断

  • A |= B:位或,左右两边的值进行位或,并将结果赋值给左边的变量。bool a |= 1 < 10; 结果为True位或:A |= B,如果B是true,那么A就是true;否则A的值不变

  • result = A ?? B:如果A是null,返回B;否则返回A

  • result ??= new List<int>():如果result是空,就进行复制操作;否则不做任何操作

  • result = A is { age: 20 }:等价result = A != null && A.age == 20


格式化字符串

用法

虽然这个应该是很常用的,但还是提一下

有两种写法,其输出结果是一样的

1
2
3
int count = 100;
Console.WriteLine("我今天吃了{0}顿", count)
Console.WriteLine($"我今天吃了{count}顿")

第二种方法的性能要比第一种好

但是第一种情况在一些特殊的情况下使用有奇效,比如多语言设置、字符串替换等功能

具体可以看C#中字符串类的一些实用技巧及新手常犯错误这个大佬的

以上用的算的比较多的,所以不是今天的重点,今天的重点在格式化插值和格式化输出

1
2
3
double i = 32.53728012341241562;

Console.WriteLine($"我的资产为{i, 10:F4}为所欲为"); // "我的资产为 32.5373为所欲为"
  • 10表示占位10个位置,当位置不够的时候在前面用空格补充
  • F4表示保留4位小数,F不区分大小写

除了F外还有其他很多格式:

数字格式

数字格式都不区分大小写

  • F:Fiexd-point格式。显示小数后几位。$"{1234567.89:F2}" => 1234567.89
  • C:货币格式。显示货币符号和千位分隔符。$"{1234.56:c}" => ¥1,234.56
  • E:科学计数法格式。显示非常大或非常小的数字。$"{1231421432:e3}" => 1.231e+009
  • N:与F类似,但是N能显示整数上的千位分隔符。$"{21432.537280:n3}" => 21,432.537
  • G:自动选择合适的格式。
  • P:百分比格式。将数值乘以100,并在结果后面加上百分号。$"{0.12345:p2}" => 12.35%
  • R:常规格式。不带任何格式的方式显示数字,但保留足够的精度(怎么保留没有仔细研究,简单测试了几下,没有找出规律)

实用方法:

1
2
float value = 546.5198;
string str = $"{value:0.##}"; // 保留两位小数,当小数不足时不显示(如,1.2 => 1.2 而不是1.20)

日期时间格式

与数字格式不同,日期时间格式需要区分大小写

注意:码代码的时候一定要拼写正确,是DateTime不是DataTime

年月日的排布DateTime会自动更具文化设置

先看看普通的格式长什么样DateTime.Now => 2024/8/11 4:50:47

  • d:短日期格式。$"{date:d}" => 2024/8/11

  • D:长日期格式。$"{date:D}" => 2024年8月11日

  • t:短时间格式。$"{date:t}" => 4:57

  • T:长时间格式。$"{date:T}" => 4:58:41

  • f:(短)完整日期和时间。$"{date:f}" => 2024年8月11日 4:59

  • F:(长)完整日期和时间。$"{date:F}" => 2024年8月11日 5:00:45

  • Mm:月日格式。$"{date:m}" => 8月11日

  • Yy:年月格式。$"{date:m}" => 2024年8月

另外还有十分强大的自定义格式化

DateTime的格式化:

1
2
DateTime date = DateTime.Now;
Console.WriteLine($"{date:yyyy年mm月dd日 hh:mm:ss tt zzz}");

输出:

1
2024年05月11日 05:05:09 上午 +08:00

TimeSpan的格式化:

1
2
3
TimeSpan timeSpan = endTime - DateTime.Now;
timeSpan.ToString(@"d\天hh\时mm\分"); // '\'必不可少,表示后面一位是普通字符
timeSpan.ToString(@"d\:h\:mm);

输出

1
2
// 1天03时15分
// 1:3:15分

Unity

性能测试

使用Profiler.BeginSample(string)Profiler.EndSample()方法捕获代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void Update()
{
Profiler.BeginSample("Invoke Action");
for (int i = 0; i < 1000000; i++)
{
actionA?.Invoke();
}
Profiler.EndSample();

Profiler.BeginSample("Invoke default Action");
for (int i = 0; i < 1000000; i++)
{
actionB.Invoke();
}
Profiler.EndSample();
}