C#技巧记录——持续更新

C# 学习笔记

一名非主修 C# 的程序员,在此记录学习与工作中遇到的 C# 有用内容,持续更新。

目录


泛型

类型约束

// 正确写法:
where T : class          // T 必须是引用类型
where T : struct         // T 必须是值类型(小写 struct)
where T : new()          // T 必须具有无参公共构造函数,且必须放在约束列表最后
where T : IUser          // T 必须实现 IUser 接口
where T : BaseClass      // T 必须继承自 BaseClass

// 组合示例
public T Create<T>() where T : class, IUser, new()
{
    return new T();
}

class 与 struct 互斥,不能同时使用;new() 必须是最后一个约束。


using 用法

创建别名(类似 C 的 typedef)

using MyInt = System.Int32;  // 为 Int32 定义别名

using static(导入静态成员)

using static System.Console;  // ⚠️ 需要写完整命名空间

class Program
{
    static void Main()
    {
        Write("Hello");  // 已导入,无需加 Console 前缀
    }
}

日期时间处理

当日 00:00:00

DateTime date = DateTime.Now;
date = date.Date;  // Date 属性返回该时间的日期部分,时分秒置为 0

当月第一天 ~ 最后一天

DateTime start = DateTime.Now.AddDays(-(int)DateTime.Now.Day + 1).Date;
DateTime end = start.AddMonths(1).AddDays(-1);

当年第一天 ~ 最后一天

DateTime start = new DateTime(DateTime.Now.Year, 1, 1);
DateTime end   = start.AddYears(1).AddDays(-1);  

当季度第一天 ~ 最后一天

int quarter = (DateTime.Now.Month - 1) / 3 + 1;              // 1 ~ 4
int startMonth = (quarter - 1) * 3 + 1;                       // 1 / 4 / 7 / 10
DateTime start = new DateTime(DateTime.Now.Year, startMonth, 1);
DateTime end   = start.AddMonths(3).AddDays(-1);

按周分组(周日划归上一周)

data.GroupBy(f => f.Day.AddDays(f.Day.DayOfWeek == DayOfWeek.Sunday
    ? -7
    : -(int)f.Day.DayOfWeek));

字符串

逐字字符串(包含特殊字符)

string longStr = @"\[][;\&";  // @ 后所有字符按字面处理,转义符失效

插值字符串

int a = 9;
string b = $"a:{a}";  // 使用变量或表达式填充

分割:匹配多个字符

str.Split('a', 'b', 'c');

分割:使用 \r\n

var infoStr = examInfo.Split(new string[] { "\r\n" },
                             StringSplitOptions.RemoveEmptyEntries);

Base64 转图片

public static void SaveImage(string logoBase64, string path, string imageName)
{
    string finalPath = Path.Combine(path, imageName);
    if (!Directory.Exists(path))
    {
        Directory.CreateDirectory(path);
    }
    File.WriteAllBytes(finalPath, Convert.FromBase64String(logoBase64));
}

条件编译

// DEBUG 是编译器预定义的特殊变量,表示是否为调试模式
// 也可以自定义:使用 #define,且必须声明在文件开始处,同 C/C++
#if DEBUG
    Console.Write("debug");
#else
    Console.Write("un debug");
#endif

#if aa
    Console.WriteLine("我是A");
#else
    Console.WriteLine("我是B");
#endif

方法参数

ref / out

// ref:传递引用,实参必须先初始化
void Show(ref int x) => Console.Write(x);
int i = 9;
Show(ref i);

// out:解决 ref 需要初始化的问题,实参可以不初始化,但方法内必须赋值
void Show(out int x) => Console.Write(x);
int j;
Show(out j);

命名参数

// 可以随意交换实参顺序:冒号前指定参数名,冒号后为值(与 Swift 一致)
public static void Show(int x, int y) => Console.Write("{0},{1}", x, y);

Show(y: 9, x: 20);

可变个数参数 params

public static void G(params int[] d)
{
    foreach (var item in d)
    {
        Console.Write(item);
    }
}

类与成员

自动实现属性

public int Age { get; set; }

静态构造函数

// 在第一次引用该类型之前自动调用,且只调用一次
class Test
{
    static int count;
    static Test()
    {
        count = 1;
    }
    public Test() { }

    public void Show() => Console.Write(count);
}

readonly

class Test
{
    readonly int count = 0;  // 只允许在声明处初始化或在构造函数中赋值

    public Test()
    {
        count = 1;
    }

    public void Show()
    {
        // count = 3;  // ❌ 编译错误
        Console.Write(count);
    }
}

构造函数初始化器

class A
{
    A(int i)
    {
        B = i;
    }

    // 自动调用相应构造函数,且在本构造函数体之前执行
    A() : this(1)
    {
    }
}

表达式体属性(类成员使用 lambda 语法)

class A
{
    int a;
    int b;
    int c => a + b;
}

override 与 new

  • 使用 override:无论通过基类还是派生类引用调用 TEST(),都会执行实际(派生)类的实现。
  • 使用 new:只是隐藏基类方法,通过类型转换到基类即可调用到基类的 TEST()。

使用保留关键字作为变量名

// 变量名前加 @,为跨平台 / 跨语言互操作提供便利
public class Test
{
    public string @class  { get; set; }
    public bool   @checked { get; set; }
}

匿名类型

// 常用于描述 JSON 对象,免去每次都新建类的麻烦
var a = new { age = 0, name = "lilei" };
Console.Write(a.age + " " + a.name);

// ⚠️ 成员之间用逗号分隔,不是分号
var zhangsan = new
{
    name = "zhangsan",
    age  = 20
};

运算符

?? 空合并运算符

int? x = null;

// 传统写法
int a;
if (x == null) { /* ... */ } else { /* ... */ }

// 合并运算符:x 非空取 x 的值,x 为空取 0
a = x ?? 0;

?. 空值传播与 ?? 空值合并

// 空指针异常写法
// string i = null;
// Console.WriteLine(i.ToString());

// 安全写法:i 为 null 时直接返回 null,输出空白
string i = null;
Console.WriteLine(i?.ToString());
Console.WriteLine("end");

var x  = new { a = "a", b = "b" };
var xx = x?.a ?? "";   // x 为空则整个表达式返回 null,再由 ?? 兜底为 ""

!. 空包容运算符(null-forgiving)

List<int> x = new();
x!.First();   // 告诉编译器「这里一定不为空」,抑制可空性警告(运行时不保证)

数组与集合

多维数组定义

// 与 C 风格不同,方括号写在类型后面
int[,] users = new int[2, 2];

排序

// 数组自带的快速排序,要求元素类型已实现 IComparable 接口
int[] te = { 555, 6, 2, 1, 65, 99, 45, 63 };
Array.Sort(te);
foreach (var item in te)
{
    Console.Write(item + "\n");
}

NameValueCollection

// 与字典不同:同一个键可以添加多次
NameValueCollection nvc = new NameValueCollection();
nvc.Add("s", "a");
nvc.Add("s", "b");

// 遍历某个键下的所有值
foreach (var item in nvc.GetValues(0))
{
    Console.WriteLine(item);
}

C# 12+ 集合表达式

string[] result = [..a, ..b];   // 展开并合并两个集合

LINQ

// 语法与 SQL 类似,不同之处在于 from 提前(便于类型推断),select 放最后
// d 为查询后的对象,in 之后为数据来源
int[] ary = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

var result = from d in ary
             where d == 1
             select d;

foreach (var item in result)
{
    Console.WriteLine(item.ToString());
}

// 可以为查询变量显式指定类型:类型错误会产生运行时异常
var result2 = from int d in ary
              where d == 1
              select d;

随机取 N 条数据

public static IQueryable<T> RandTake<T>(this IQueryable<T> query,
                                        Expression<Func<T, bool>> predicate,
                                        int count)
{
    return query.Where(predicate)?.OrderBy(a => Guid.NewGuid())?.Take(count);
}

委托与 Lambda

Action / Func

void a() { }
int  b() { return 0; }

void Test()
{
    Action   ax = new Action(a);
    Func<int> af = new Func<int>(b);
}

Lambda 表达式

int GetDouble(int x) => x * x;   // 这是「表达式体方法」,不是局部 lambda

[InstantHandle]

标识该 lambda 是立即执行的,可以用此特性通过 lambda 把运行时参数传递给后续回调(JetBrains 注解特性)。


反射与枚举

获取枚举的 [Description("")]

public static class EnumHelper
{
    public static string GetDescription(this Enum enumeration)
    {
        Type type = enumeration.GetType();
        MemberInfo[] memInfo = type.GetMember(enumeration.ToString());
        if (null != memInfo && memInfo.Length > 0)
        {
            object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
            if (null != attrs && attrs.Length > 0)
                return ((DescriptionAttribute)attrs[0]).Description;
        }
        return enumeration.ToString();
    }
}

获取枚举的所有值

public static Array GetAll(this Enum e)
{
    return Enum.GetValues(e.GetType());
}

获取对象的 Description

public static string Description(this object AEnum)
{
    Type type = AEnum.GetType();
    MemberInfo[] memInfo = type.GetMember(AEnum.ToString());
    if (null != memInfo && memInfo.Length > 0)
    {
        object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
        if (null != attrs && attrs.Length > 0)
            return ((DescriptionAttribute)attrs[0]).Description;
    }
    return AEnum?.ToString();
}

类型判断与转换

is / as

// is:返回判断结果 true / false
// as:返回转换后的引用,转换失败返回 null
// 两者失败时都不抛异常

类型转换

Convert.ChangeType(value, property.PropertyType);

static 与 const

  • static:表示静态的。
  • const:表示静态的常量。

其他关键字

nameof

// 获取方法、类、变量的名称(编译期常量,重命名时自动同步)
nameof(SomeMethod);

default 获取类型默认值

int i = default(int);   // C# 7.1+ 可简写为:int i = default;

checked / unchecked

// 检测代码块在计算过程中是否发生溢出
// 默认为不检测(unchecked),一般不需要显式写 unchecked
checked
{
    int x = int.MaxValue + 1;   // 抛出 OverflowException
}

checked 作为属性名(关键字冲突)

public bool @checked { get; set; }

文件与 IO

共享模式写文件

using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate,
                                      FileAccess.ReadWrite, FileShare.ReadWrite))
{
    fs.SetLength(0);
    using (StreamWriter writer = new StreamWriter(fs, Encoding.Default))
    {
        string infos = JsonConvert.SerializeObject(info.data);
        writer.Write(infos);
        writer.Flush();
    }
}

共享模式读文件

using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate,
                                      FileAccess.ReadWrite, FileShare.ReadWrite))
{
    // ⚠️ 读取时不要 SetLength(0),否则内容会被清空
    using (StreamReader reader = new StreamReader(fs))
    {
        string lifeStr = reader.ReadToEnd();
    }
}

using 块结束时会自动 Dispose,无需手动调用 writer.Dispose() / fs.Dispose()。

随机文件名与临时目录

Console.WriteLine(Path.GetRandomFileName());  // 随机文件名
Console.WriteLine(Path.GetTempFileName());    // 临时文件(会自动创建文件)
Console.WriteLine(Path.GetTempPath());        // 临时目录

路径与编码

获取运行路径

Directory.GetCurrentDirectory();                        // 当前工作路径(可能变化)
Environment.CurrentDirectory;                           // 当前工作路径
AppDomain.CurrentDomain.BaseDirectory;                  // 程序文件所在路径(推荐)
AppDomain.CurrentDomain.SetupInformation.ApplicationBase; // 程序文件所在路径

指定字符集报错

'GB2312' is not a supported encoding name. For information on defining a custom encoding,
see the documentation for the Encoding.RegisterProvider method.
// .NET Core 默认不含 GB2312 等编码,需先注册
Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
var bytes = Encoding.GetEncoding("GB2312").GetBytes(str);

MIME 类型预定义字符串

系统已自带,无需自定义:见 System.Net.Mime.MediaTypeNames(MediaTypeNames.Text.Plain、MediaTypeNames.Application.Json、MediaTypeNames.Image.Jpeg 等)。


WPF

ShowActivated

指示窗口在第一次显示时是否处于激活状态(ShowActivated="False" 可避免抢焦点)。

获取控件所属的窗口

Button button = sender as Button;
Window targetWindow = Window.GetWindow(button);

获取主窗口

Application.Current.MainWindow

检测窗口是否已经打开过

// ⚠️ 窗口对象引用在窗口关闭后并不会变成 null,不能用 null 判断
public static bool IsAlive(this Window window)
{
    return Application.Current.Windows.OfType<Window>().Contains(window);
}

Entity Framework

// 关闭状态追踪
.AsNoTracking()

// 关闭状态同步
context.ChangeTracker.AutoDetectChangesEnabled = false;

EF 中跟踪的数据都记录在 db.Set<T>().Local 对象中。如果同一事务中修改并保存了变更,后续再次变更同一实体时不得重复跟踪,需先移除跟踪:

public T Update(T entity)
{
    var localData = db.Set<T>().Local.FirstOrDefault(e => e.Id == entity.Id);
    if (localData != null)
    {
        db.Entry(localData).State = EntityState.Detached;
    }

    db.Set<T>().Attach(entity).State = EntityState.Modified;
    return entity;
}

Migration 指定上下文

Update-Database -Context BookStoreDbContext

工程与工具链

指定 C# 语言版本

默认语言版本与目标框架(TFM)保持一致(如 .NET 9 → C# 13);不建议设 latest,因为该值随机器安装的编译器变化,会导致构建不可靠[citation:2]。

<PropertyGroup>
   <LangVersion>preview</LangVersion>
</PropertyGroup>
值 含义
preview 编译器接受最新预览版中的所有有效语言语法
latest 编译器接受最新发布的编译器版本(含次要版本)中的语法
latestMajor(默认) 编译器接受最新发布的编译器主要版本中的语法
10.0 只接受 C# 10 或更低版本中的语法
9.0 只接受 C# 9 或更低版本中的语法
8.0 只接受 C# 8.0 或更低版本中的语法
7.3 / 7.2 / 7.1 / 7 只接受对应版本或更低版本中的语法
6 / 5 / 4 / 3 只接受对应版本或更低版本中的语法
ISO-2(或 2) 只接受 ISO/IEC 23270:2006 C# (2.0) 中的语法
ISO-1(或 1) 只接受 ISO/IEC 23270:2003 C# (1.0/1.2) 中的语法

不支持使用比目标 TFM 关联版本更新的 C# 语言版本[citation:2]。在代码中添加 #error version 可让编译器报告当前语言版本[citation:2]。

参考:配置语言版本 - C# reference[citation:2]

发布单个 exe

dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true

发布指定操作系统、架构到指定路径

dotnet publish D:\WebAPI.sln --os win -a x64 -c Release --property:PublishDir=D:\publish

配置 NuGet 源

dotnet nuget add source [仓库地址] -n [仓库名称]

命令行更新项目中的单个 NuGet 包

nuget.exe update X.csproj -Id PackageA

项目版本号

<PropertyGroup>
    <VersionPrefix>1.2.0</VersionPrefix>
</PropertyGroup>

控制台程序隐藏执行

修改项目文件(.csproj),或将项目输出类型改为「Windows 应用程序」:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>  <!-- 而不是 Exe / Console -->
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
</Project>

编译后复制文件夹(生成事件)

xcopy "$(ProjectDir)\Assets" "$(TargetDir)\Assets\" /S /Y

显示 TODO 标签

视图 → 任务列表

VS 配置命名规范

工具 → 选项 → 文本编辑器 → C# → 代码样式 → 命名

ILDASM 工具位置

C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools

杂项

AppContext(动态开关)

// 设置开关,可用于实现动态切换:如配置文件为 DEBUG 模式则自动添加测试数据
AppContext.SetSwitch("Switch.AmazingLib.ThrowOnException", true);
// 读取开关
public class AmazingLib
{
    private bool shouldThrow;

    public void PerformAnOperation()
    {
        if (!AppContext.TryGetSwitch("Switch.AmazingLib.ThrowOnException", out shouldThrow))
        {
            // 应用未设置该开关,库可选择其他方式获取,默认 false(即最新行为)
        }

        if (shouldThrow)
        {
            // old code
        }
        else
        {
            // new code
        }
    }
}

Lazy<T> 延迟加载

public class Student
{
    public Student()
    {
        this.Name = "DefaultName";
        this.Age  = 0;
        Console.WriteLine("Student is init...");
    }

    public string Name { get; set; }
    public int    Age  { get; set; }
}

Lazy<Student> stu = new Lazy<Student>();
if (!stu.IsValueCreated)
    Console.WriteLine("isn't init!");

Console.WriteLine(stu.Value.Name);   // 首次访问 Value 时才真正构造
stu.Value.Name = "Tom";
stu.Value.Age  = 21;
Console.WriteLine(stu.Value.Name);

WeakReference 弱引用

弱引用:在引用对象的同时,允许 GC 回收该对象。适用于「创建便宜但占用大量内存」、既希望保留又希望在内存紧张时被回收的对象。

自宿主 WebAPI(Minimal API)

Task.Run(() =>
{
    var builder = WebApplication.CreateBuilder(new string[] {
        "--urls", "http://localhost:8888",
    });
    var app = builder.Build();
    app.MapGet("/", () => "Hello .net mini api");
    app.Run();
});

Socket 发送文件(打印)

string fileName = "PDF_2224101MV.pdf";
string filePath = Path.Combine(Environment.CurrentDirectory, fileName);
byte[] buffer;

using (Stream stream = new FileStream(filePath, FileMode.OpenOrCreate))
{
    buffer = new byte[stream.Length];  
    stream.Read(buffer, 0, buffer.Length);
}

Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.NoDelay = true;
IPAddress ip = IPAddress.Parse("10.159.4.28");
IPEndPoint endPoint = new IPEndPoint(ip, 9100);
clientSocket.Connect(endPoint);
clientSocket.Send(buffer);
clientSocket.Close();

byte / sbyte

  • byte:无符号(0 ~ 255)
  • sbyte:有符号(-128 ~ 127)

Microsoft.Windows.Compatibility

从 Framework 迁移到 .NET Core 时缺失了一部分 API(如 Drawing、EventLog、WMI、Performance Counters),该兼容包中基本都有补充。

posted @ 2017-04-22 19:45  Hey,Coder!  阅读(585)  评论(0)    收藏  举报