学习C#
下面的案例和代码算不上特别基础,关于基础的C#,网上一大堆,我决定重新看一遍C#本质论,
然后记录下,用来完善我的基础,朋友们可以跳着看
1、开始基础
a、解释下string的不可变
string result;
result="HelloWorld!";
在C#中,string 类型是一个引用类型,这意味着它在堆上分配内存。当你声明一个 string 类型的变量 string result; 时,你创建了一个引用变量 result,但此时它并没有指向任何对象,也就是说它没有指向堆上的任何 string 对象,因此它没有大小、长度或生命周期。
当你执行 result="HelloWorld!"; 这行代码时,发生了以下变化:
堆上分配:在堆上为 "HelloWorld!" 这个字符串分配了内存空间,并且创建了一个 string 对象。
引用赋值:result 这个引用变量被赋值为指向堆上新创建的 string 对象的引用。
大小和长度:此时 result 指向的 string 对象有了大小(占用的内存空间)和长度(字符串中字符的数量,这里是12个字符)。
生命周期:string 对象的生命周期开始,直到没有任何引用指向它,才会被垃圾回收器回收
internal class Program
{
static void Main(string[] args)
{
string str = "123";
str = "456";
Console.WriteLine(str);
}
}
如果你是一个新手,你可以先简单理解下。int i=1;i=2,假设有一个圆圈,代表一块内存,这块内存里面的从1变为了2,表示int是可变的, string str = "123";str = "456";假设有两块内存a和b,123在a中,456在b中,str的变化只是指向了b,但是a内存的123不变,然后这个类Program执行完毕后a内存没有被引用就被垃圾回收
如果你有了一定的基础,我再继续说
在栈上为变量 str 分配内存空间,str 是一个引用,指向一个 string 对象。
在堆上为字符串 "123" 分配内存空间,并创建一个 string 对象。由于 C# 的字符串字面量池机制,如果池中已经存在 "123" 这个字符串,那么 str 将直接引用池中的 "123" 对象
b、分析下C#和java的执行
C# 编译过程:
C# 源代码 (.cs 文件):首先,开发者编写 C# 代码,保存为 .cs 文件。
编译成 CIL (Common Intermediate Language):C# 编译器(如 csc.exe)将 .cs 文件编译成 Microsoft 中间语言(CIL),也称为 MSIL(Microsoft Intermediate Language)。CIL 是一个与平台无关的中间代码,类似于 Java 的字节码。
转换为机器码:然后,CIL 代码通常由 .NET Framework 的 Just-In-Time (JIT) 编译器在运行时转换为特定平台的机器码。这个过程称为 JIT 编译。在 .NET Core 和 .NET 5/6+ 中,也有一个预编译的选项,可以将 CIL 编译为平台特定的机器码。
执行:生成的机器码随后由操作系统执行。
Java 编译过程:
Java 源代码 (.java 文件):开发者编写 Java 代码,保存为 .java 文件。
编译成字节码 (.class 文件):Java 编译器(javac)将 .java 文件编译成 Java 字节码,这是一种与平台无关的中间代码。
运行时转换:Java 字节码不是直接转换为机器码,而是在 Java 虚拟机(JVM)上运行。JVM 是 Java 字节码的运行时环境。
JIT 编译:JVM 中也包含 JIT 编译器,它在运行时将 Java 字节码转换为特定平台的机器码,以提高性能。
执行:转换后的机器码由操作系统执行。
c、说下using的一种用法
using static System.Console;
using static Using关键字.Student;
namespace Using关键字
{
/// <summary>
/// using除了释放内存之外,还有using static的使用
/// </summary>
internal class Program
{
static void Main(string[] args)
{
WriteLine("Hello, World!");
Id = 1;
}
}
public class Student
{
public static int Id { get; set; }
}
}
可以明显的看出,我们打印直接写了WriteLine,还有直接写出了外部类的属性Id
d、理解null和""
string str="";
string str2=null;
不用死记硬背,你就理解电话号码是空和未知电话
e、基元类型静态方法TryParse
//C#7.0开始不用事先声明out 参数的变量
var s = int.TryParse("123", out int str3) ? "转换正常":"转换失败";
Console.WriteLine(s);
这种写法很好,我们可以试着扩展C#没有字符串转化Int,ToInt方法如下
internal class Program
{
static void Main(string[] args)
{
string value = "123";
Console.WriteLine(value.ToInt());
Console.WriteLine(value.TryToInt(out int number));
}
}
public static class StringExtension
{
public static int ToInt(this string str)
{
int result = 0;
if (string.IsNullOrWhiteSpace(str))
{
return result;
}
try
{
return int.Parse(str);
}
catch (FormatException)
{
// 可以选择返回一个特殊值,比如 -1,或者抛出一个新的异常
return -1;
}
}
//请注意,[NotNullWhen(true)] 属性通常用于指示当方法返回 true 时,输出参数不为 null。在这个场景中,它并不适用
public static bool TryToInt(this string str, out int result)
{
if (string.IsNullOrWhiteSpace(str))
{
result = 0; // 或者你可以选择返回 -1 或其他默认值
return false;
}
// 尝试转换字符串到整数
return int.TryParse(str, out result);
}
}
2、数据类型
1-1、int在栈,string在堆+字面量池
int i=2;
在C#中,int i=2;那么这句代码写了后,i是变量,2是存储的数据,i和数字2都在内存中,int是值类型,所以i和2都在栈中
1-2、操作符
三元运算符?:
public bool IsCheck=>Create();
string str=IsCheck?"运算正确":"运算错误"
空运算符??
private ObservableCollection<Student> _students;
public ObservableCollection<Student> Students
{
get =>_students ?? _students=new ObservableCollection<Student>();
set =>_students=value;
}
空条件符?. 最适合委托书写
PropertyChanged?Invoke(propertyChanged(this,new PropertyChangedEventArgs(nameof(Name))));
按位操作符 (<<>>,|,&,^,~) 适合处理二进制
1-3、多个表达式for循环
for(int x=0,y=5;((x<=5)&&y>=0);x++,y--)
{
Console.WriteLine($"{x}{((x>y) ? '>' : '<')}{y}\t");
}
0<5
1<4
2<3
3>2
4>1
5>0
1-4 代码的可读性
开发者应注重代码的可读性,而不应该以把过多的精力放在写简短的代码上
现在我们读取本地一个名称为Login.json的文件内容,代码如下
Public class Demo
{
public void Usage()
{
FileQuerier fileQuerier=new FileQuerier()
{
FileType=FileType.Json,
FileName="Login",
}
fileQuerier.Load<T>();
}
}
某些方法过于简单直接使用表达式
public bool IsLogin => (判断登录条件) ? true : false;
1-5 using 别名的使用
尤其是WPF使用HandyControl之后,Window和MessageBox经常与原生的混淆而报编译错误
所以我经常使用HC库后
using HandyOrgWindow=HandyControl.Controls.Window;
using HandyOrgMessageBox = HandyControl.Controls.MessageBox;
这样使用的时候
HandyOrgMessageBox.Show("登录成功");
1-6 连接地址
使用HttpClient读取网络地址,或者Json读取本地地址我们可以使用Combine方法
string path1 = "C://";
string path2 = "index.html";
var path = Path.Combine(path1, path2);
这样写可以使代码看起来清爽,而不使用+相连接
3、抽象类
1-1 继承
public class Demo1
{
private string Name { get; set; }
1、 public Demo1(string name)
{
Name= name;
Console.WriteLine(Name);
}
}
public class SubDemo1 : Demo1
{
2、 public SubDemo1(string name) : base(name)
{
3、 Console.WriteLine(name);
}
}
public class TestDemo1
{
public static void Usage()
{
SubDemo1 subDemo1 = new SubDemo1("张三");
}
}
上面代码标记的123你觉得执行顺序是怎样的?
答案是213
父类的Name是靠子类传输过去的,基于这种,我想到之前的SqlSugar的IOC注册
它也是将子类的ConnectionObject靠DbScoped传递给父类
具体代码可以看我之前的博客。SqlSugar的连接方式
1-2 protected
protected只能在基类和派生类使用
1-3 继承链
先猜猜下面打印出来是什么
public class BaseClass
{
public void DisplayName()
{
Console.WriteLine("第一阶段");
}
}
public class DerivedClass : BaseClass
{
public virtual void DisplayName()
{
Console.WriteLine("第二阶段");
}
}
public class SubDerivedClass : DerivedClass
{
public override void DisplayName()
{
Console.WriteLine("第三阶段");
}
}
public class SuperSubDerivedClass : SubDerivedClass
{
public new void DisplayName()
{
Console.WriteLine("第四阶段");
}
}
public class TestDemo2
{
public static void Usage()
{
SuperSubDerivedClass superSubDerivedClass = new SuperSubDerivedClass();
SubDerivedClass subDerivedClass=superSubDerivedClass;
DerivedClass derivedClass= superSubDerivedClass;
BaseClass baseClass = superSubDerivedClass;
superSubDerivedClass.DisplayName();
subDerivedClass.DisplayName();
derivedClass.DisplayName();
baseClass.DisplayName();
}
}
答案是,四 三 三 一
1、,BaseClass baseClass = superSubDerivedClass;相当于多态,所以baseClass 调用方法打印的结果是第一阶段,
2、superSubDerivedClass.DisplayName(); 输出 "第四阶段",这是它本身new的自己,并且它没有子类,打印自己的方法没问题,
3、subDerivedClass.DisplayName(); 输出 "第三阶段",它使用override,重写了父类的方法,打印本身没问题,
4、关键是derivedClass.DisplayName(); 输出 "第三阶段"也是相当于多态,按理来说打印的应该是本身,
但是它使用了virtual,它的子类使用了override,所以它打印的是自己子类的输出,也就是第三阶段
1-4 多态与switch的组合使用
public static void Save(Storage storage)
{
switch(storage)
{
case null:
throw new ArgumentNullException(nameof(storage));
case Usb usb when usb.IsPluggedIn:
usb.Unload();
Console.WriteLine("USB Drive Unloaded");
break;
case Dvd dvd when dvd.IsInserted:
dvd.Eject();
Console.WriteLine("DVD Ejected");
break;
}
}
public class Usb : Storage
{
public bool IsPluggedIn { get; private set; }
public void Unload()
{
// 实现USB卸载逻辑
}
}
public class Dvd : Storage
{
public bool IsInserted { get; private set; }
public void Eject()
{
// 实现DVD弹出逻辑
}
}
public abstract class Storage
{
// Storage类可能包含一些所有存储设备共有的属性和方法
}
4、接口
1-1 显式接口
public interface IReadableService
{
string GetSetting(string name,string defaultValue);
}
public interface ISettingProvider:IReadableService
{
void Setting(string name,string defaultValue);
}
public class FileSettingProvider : ISettingProvider, IReadableService
{
public string GetSetting(string name, string defaultValue)
{
throw new NotImplementedException();
}
public void Setting(string name, string defaultValue)
{
throw new NotImplementedException();
}
string IReadableService.GetSetting(string name, string defaultValue)
{
throw new NotImplementedException();
}
}
隐藏实现细节:当您不希望类的外部访问者直接调用接口中定义的方法,而是希望提供更具体的实现时,可以使用显式接口实现。
解决命名冲突:当类实现了多个接口,并且这些接口中有相同签名的方法时,显式接口实现可以用来区分这些方法的不同实现。
多态性:显式接口实现允许类以不同的方式实现同一个接口中的方法,这可以提供更细粒度的控制。
1-2 版本控制
如组件或应用程序正在供其他开发者使用,创建新版本时,不要修改接口。接口在实现接口的类和使用接口的类之间订立了契约,修改接口相当于修改契约,会使接口写的代码失效。
1-3 接口和特性
有时无任何成员的接口(不管是不是继承的)来描述关于类型的信息。例如,有人会创建IObsolete来标记接口指出某类型已被另一个类型取代,一般认为这是对接口机制的"滥用",接口应表示类型能执行的功能,而非陈述关于类型的事实。这时不要使用接口,使用特性,这是C#本质论的原话。
Furion框架自从收费以后被很多人诟病,它不是一个框架而是一个工具程序集的集合
它里面空的接口太多,对接口造成了滥用
1-4 抽象类和接口的选择
抽象类 | 接口 |
---|---|
不能直接实例化,只能实例化一个派生类 | 不能直接实例化,只能实例化一个实现类型 |
派生类要么自己也是抽象的,要么必须实现所有抽象成员 | 实现类型必须实例化所有接口成员 |
可添加额外的非抽象成员,由所有派生类继承,不会破坏跨版本兼容性 | 为接口添加额外的成员会破坏版本兼容性 |
可声明方法,属性和字段(以及其他成员类型,包括给构造函数和终结器) | 可声明方法和属性但不能声明字段、构造函数或终结器 |
成员可以是实例、虚、抽象、或静态、非抽象成员可以提供默认实现供派生类使用 | 所有成员都基于实例(而非静态),而且自动视为抽象,所以不能包括任何实现 |
派生类只能从一个基类派生(继承) | 实现类型可实现任意多的接口 |
需要注意的是,我上面的表格是C#7.0本质论写的,现在的C#接口可以实现静态的属性,可以写自己的方法,随着版本的更新,它不是准确的,你们要自己辨别
5、泛型
1-1、泛型类
public class Sample1<T>
{
public int IComparer(T t)
{
return -1;
}
}
1-2、泛型方法
public class Sample2
{
public int IComparer<T>(T t)
{
return -1;
}
}
1-3、泛型接口
public interface IPair<T>
{
T First { get; set; }
T Second { get; set; }
}
public class Pair<T> : IPair<T>
{
public Pair(T first,T second)
{
First = first;
Second = second;
}
public T First { get; set; }
public T Second { get; set; }
}
但是这会出现一个问题,泛型Pair构造器如果只有一个参数,Second没办法写默认值,因为不知道T的类型。
这个时候default关键字就出来了
public Pair(T first)
{
First = first;
Second = default(T);
}
也可以使用多个泛型参数
public interface IStudent<TFirst, TSecond>
{
TFirst First { get; set; }
TSecond Second { get; set; }
}
1-4、泛型约束
public class BinaryTree<T>
{
public T Item { get; set; }
public Pair<BinaryTree<T>> _subItems;
public Pair<BinaryTree<T>> SubItems
{
get => _subItems;
set
{
IComparable<T> first;
first = (IComparable<T>)value.First.Item;
if (first.CompareTo(value.Second.Item)<0)
{
}
_subItems = value;
}
}
}
1-5、构造函数约束
public Student<TKey,TValue>() where TKey:IComparable<TKey> where TValue:EntityBase<TKey>,new()
{
}
并非所有的对下个都有公共的默认构造函数,new关键字添加后,要求实参必须要有默认的构造函数,它不能对有参构造函数指定约束。
泛型促进了类型安全,确保在参数化的类中,只有成员明确希望的数据类型才能可使用。
泛型缓解了代码膨胀,既保持了具体版本的优势,又没有具体版本的开销。
性能得以提高,一个原因是不再需要从object的强制转换,从而避免了类型检查,另一个原因是不需要为值类型装箱。
内存消耗减少,由于避免了装箱,因此减少了堆上的内存消耗。
最核心的是,泛型允许写代码来实现模式,并在以后出现这种模式的时候重用该实现。而泛型为这些反复出现的模式提供了单一的实现。
/// <summary>
/// 来考虑一个恒温控制的例子,一个加热器、一个冷却器、一个恒温器
/// </summary>
public class TestDemo
{
public static void Usage()
{
Thermostat thermostat = new Thermostat();
Heater heater=new Heater(60);
Cooler cooler = new Cooler(80);
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
Console.WriteLine("------------开始调节温度---------------");
while (true)
{
Console.Write("请输入当前温度: ");
string input = Console.ReadLine();
if (input.ToLower() == "exit")
{
Console.WriteLine("程序退出。");
break; // 退出循环
}
if (int.TryParse(input, out int temperature))
{
thermostat.CurrentTemperature = temperature;
}
else
{
Console.WriteLine("输入无效,请输入一个数字或 'exit' 退出。");
}
}
}
}
/// <summary>
/// 订阅者
/// </summary>
public class Cooler
{
public float Temperature { get; set; }
public Cooler(float temperature)
{
Temperature = temperature;
}
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature>Temperature)
{
Console.WriteLine("冷却器打开");
}
else
{
Console.WriteLine("冷却器关闭");
}
}
}
/// <summary>
/// 订阅者
/// </summary>
public class Heater
{
public float Temperature { get; set; }
public Heater(float temperature)
{
Temperature = temperature;
}
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature < Temperature)
{
Console.WriteLine("加热器打开");
}
else
{
Console.WriteLine("加热器关闭");
}
}
}
/// <summary>
/// 发布者
/// </summary>
public class Thermostat
{
public Action<float> OnTemperatureChange { get; set; }
private float _currentTemperature;
public float CurrentTemperature
{
get => _currentTemperature;
set
{
if (value !=_currentTemperature)
{
_currentTemperature = value;
OnTemperatureChange(value);
}
}
}
}