chapter3 方法与作用域
-
方法其实就是函数,只是在c#里必须定义在类内部
chapter7 创建并管理类和对象
-
类和对象不要混淆。类是类型的定义,对象则是该类型的实例,是在程序运行时创建的。换言之,类是建筑蓝图,对象是按照蓝图建筑的房子。同一个类可以有多个实例,正如同一张蓝图可以构建很多栋房子。
-
类里定义的变量成为字段,函数称为方法。
-
声明私有字段或方法需要添加关键字private,默认添加的就是该关键字。
-
方法中声明的变量不会被初始化,但类的字段会被自动化初始化为0,false或null。
-
类可以被拆分到多个不同文件进行定义,需要在每个文件中使用partial关键字定义类的不同分。
-
this关键字可以帮助区分字段和参数,当参数和字段重名时,通过this.字段来表示该字段是类里的字段而不是参数。
-
私有是类级别的私有而不是对象级别的私有,同一个类的两个实例能互相访问私有数据,但访问不了其他类的实例中的私有数据。
-
构造器创建并初始化对象(通常是填充它包含的字段)。解构器则检查对象并提取它的字段的值,解构器只能命名为Deconstruct.
-
并非所有方法都天生属于类的某个实例,这些称为工具方法或者实用方法,通常提供了有用的、和类的实例无关的功能。
-
c#所有方法都必须在类的内部声明,但如果把方法或字段声明为static,就可以用类名调用方法和访问字段。静态方法不依赖类的实例,不能在其中访问类的任何实例字段和实例方法,相反,只能访问标记为static的其他方法和字段。
-
静态字段能在类的所有对象之间共享,非静态字段则局部于类的实例。
-
c#允许声明静态类,静态类只包含静态成员,使用该类创建的所有对象都共享这些成员的单一拷贝。
-
私有和静态是两码事,字段声明为私有,类的每个实例都有一份自己的数据。声明为静态,每个实例都共享同一份数据。
-
严格来说,c#中没有全局变量和全局函数的说法,但可以通过声明Public静态字段和静态方法来模拟实现。
chapter8 理解值与引用
-
引用类型和值类型,可以将引用类型理解为指针,类类型就是引用类型。circle c = new cicle();c只是指向cicle的一个对象的指针,而此对象真正的内存通过new创建。再有,circle d = c;d只是指向了和c一样的地址,换言之,现在只有一个circle对象,d和c都引用它。
-
可以在c#代码里继续使用指针,但必须将代码标记为unsafe
-
向方法传递实参时,对应的参数(形参)通常会用实参的拷贝来初始化,不管参数是值类型、可空类型还是引用类型。(和c语言一样);但有少数情况下,希望改变实参,这就需要用到ref和out关键字
-
所有值类型都在栈上创建,所有引用类型的实例(对象)都是在堆里创建,引用本身还是在栈上。
-
.NET Framwork最重要的引用类型之一是System命名空间中的Object类:所有类都是Object的派生类,Object类型的变量可以引用任何对象。由于Object非常重要,c#提供了object关键字来作为Object的别名。
-
装箱,object定义的变量可以引用任何对象也包括值类型,但object定义的变量是引用类型,值类型是分配在栈上的,直接引用不安全,c#会在堆上自动分配内存来复制这个值类型变量。这种将数据从栈自动复制到堆的行为叫做装箱。
chapter9 使用枚举和结构创建值类型
-
c#支持两种值类型,枚举和结构;int、short、bool等这种属于简单类型。
-
枚举的基础类型是可选的,enum xxx :byte{ },这个是指xxx基于byte数据类型,则其最多容纳256个字面值(从0开始)。
-
类定义的是引用类型,总是在堆上创建。有时类只包含极少数据,因为管理堆而产生的开销不合算。此时更好的做法是将类型定义成结构。结构是值类型,在栈上存储,能有效减少内存管理的开销(当然前提是该结构足够小)。
-
结构可以包含自己的字段、方法和构造器(但不能主动声明默认构造器)。int等都是System.Int32结构的别名
chapter10 使用数组
-
数组是无序的元素序列,数组中所有元素都具有相同类型,数组中的元素存储在一个连续性的内存块中,并通过索引来访问。(定义上和c语言一样)
-
无论数组元素是什么类型,数组始终都是引用类型。这意味着数组变量引用堆上的内存卡,数组元素都存在这个内存块中,和类变量引用堆上的对象一样。
-
c#数组声明可以不声明数组大小,如int aaa[];创建数组实例时,用new关键字,aaa = new int[4];由于数组实例的内存动态分配,所以数组实例的大小不一定是常量,可以定义 aaa = new int[b]。创建实例后,所有元素都被初始化为默认值(取决于元素类型,数值为0,引用为null)。
-
所有数组都是Microsoft.NET Framework的System.Array的实例,该类定义了许多有用的属性和方法,比如Length属性可以直接查出数组的长度,方便遍历数组等操作。
-
c#创建多维数组,int[,] items = new[4,6]。创建包含24个整数的二维数组,4行6列。
chapter11 理解参数数组
-
如方法获取数量可变、类型也可能不同的实参,就可以考虑使用参数数组,当然也可以使用函数重载的方式,但是如果参数数量变化很多,使用重载就比较麻烦,因为需要太多个重载方法。
-
使用参数数组需要使用关键字params,该关键字可以帮助你自动创建数组。例如,通过以下允许在方法调用中传递任意数量的int参数
class Util
{
public static int Min(params int[] paramlist)
{
if(paramlist == null || paramlist.Lenth == 0)
{
throw;
}
int min = paramlist[0];
foreach(int i in paramlist)
{
if (i < min)
{
min = i;
}
}
return min;
}
}
//当你使用如下代码时
int min = Util.Min(aaa,bbb);
//系统通过prarms关键字已经帮你转化成如下代码
int[] array = new int[2];
array[0] = aaa;
array[1] = bbb;
int min = Util.Min(array);
-
如果方法的参数类型也不固定,可以用object关键字,如:
class Black
{
public static void Hole(params object[] paramList)
...
}
//传递不同类型的实参,这些实参自动包装到object数组中:
Black.Hole("forty two",42);//被转换成Black.Hole(new object[]{"forty two",42});
chapter 12 使用继承
-
派生类继承得到基类的方法,自动包含基类的所有字段。创建对象时,这些字段通常需要初始化,作为好的编程实践,派生类的构造器在执行初始化时,最好调用一下它的基类构造器,为派生类定义构造器时,可以用base关键字调用基类构造器。
-
派生类中的方法会屏蔽(隐藏)基类具有相同签名的方法,但编译器会发出警告,使用new可以消除这种警告。
-
虚方法,基类中用virtual定义的方法,派生类可以用override关键字重写。
-
受保护关键字,pretected。类成员被不同关键字定义后的使用范围,public > protected > private。派生类能访问受保护的基类成员,派生类的派生类也能。
-
扩展方法,允许添加静态类中的定义,被扩展类型必须是方法的第一个参数,必须附加this关键字。例如:
static class Util
{
public static int Negate(this int i)
{
retun -i;
}
}
{
using xxx ;//Util所在的命名空间
int x = 591;
Console.WriteLine($"x.Negate(){x.Negate()}");
}
chapter 13 创建接口和定义抽象类
-
接口不包含任何代码和数据,它只是规定从接口继承的类必须提供哪些方法和属性。用关键字interface定义接口。不可以向接口添加字段。一个类可以从一个类继承的同时实现接口,首先写基类名,再写逗号,最后写接口名。一个接口可从另一个接口继承,技术上说应该叫接口扩展而不是继承。
-
接口与虚方法,虚方法局限于由同一基类继承的类的实例中去做不同实现,接口容纳的方法,可以使用的对象不必存在这种继承关系。
-
可以通过接口引用类。例如:
interfaceI IlandBound
{
...
}
class Mannal
{
...
}
class Horse:Mannal,ILandBound
{
...
}
//下面就是通过接口引用类
{
Horse myHorse = new Horse(...);
IlandBound iMyHorse = myHorse;//合法,但其只能调用接口内的方法
}
-
接口的显示实现与隐式实现,主要体现在调用方式的区别,显示实现的接口,接口成员必须通过接口类型的引用来调用。这是因为显式实现的成员不会出现在类的成员列表中,所以不能通过类实例直接访问。
public interface IFlyable
{
void Fly();
}
public class Bird : IFlyable
{
public void Fly() //隐式
{
Console.WriteLine("Bird is flying.");
}
}
public class Bird : IFlyable
{
void IFlyable.Fly() //显式
{
Console.WriteLine("Bird is flying.");
}
}
-
显式实现通常用于以下情况:
-
类实现了多个接口,其中包含了同名的成员。
-
不希望接口成员成为类的公共成员,而是仅限于通过接口访问。
-
需要在同一个类中实现不同的接口行为,每个接口具有相同的成员签名。
-
抽象类,因为一个接口可以由多个类继承,继承类必须实现接口定义的方法,这就避免不了有些通用性方法的重复实现,可以定义一个派生类,专门实现这种通用的方法,后续类再从派生类继承。为了限制这种派生类只能作为基类,即不想这种派生类可以创建出其他实例,加上关键字abstract。
-
密封类及密封方法。密封类使用关键字sealed,不能作为基类使用,密封方法声明要用sealed override,密封方法不能被重写。
chapter14 使用垃圾回收和资源管理
-
值类型在栈上创建,引用类型分配的是堆内存,值类型离开作用区域就会被销毁。c#中没有delete操作符,销毁对象并将内存归还给堆的过程(即垃圾回收)全部由CLR控制。
-
CLR:Common Language Runtime,通常翻译为公共语言运行时或公共语言运行库)扮演着至关重要的角色。CLR是.NET Framework和.NET Core/.NET的一部分,它提供了一种标准的运行时环境,能够执行多种语言编写的代码。
-
在C#编程中,“托管”(Managed)通常指的是由.NET运行时(如CLR, Common Language Runtime)直接管理和执行的代码和资源。当我们在C#中编写代码并编译时,生成的是中间语言(IL,Intermediate Language)代码,而不是直接的机器代码。这个IL代码在运行时会被CLR即时编译(JIT, Just-In-Time Compilation)成特定平台的机器代码。这一过程和随后的代码执行都处于CLR的“托管”之下。
-
非托管资源,例如文件流、网络连接、数据库连接和其他由windows操作系统管理的资源。CLR能自动清理对象使用的任何托管资源。
-
由CLR控制的垃圾回收不一定在对象不再需要的之后马上进行,垃圾回收可能是一个代价比较高的过程,所以“运行时”只有在觉得必要的时候进行垃圾回收,比如,它认为内存不够的时候,或者堆的大小超过系统定义的阈值的时候。
-
有些资源过于宝贵,用完后应该马上释放,而不是等待垃圾回收器在将来不确定的时间释放。内存、数据库连接、文件句柄等稀缺资源应尽快释放,这时唯一的选择就是亲自释放资源,通过自己写的资源清理(disposal)方法实现。
-
using语句和IDisposable接口,using语句提供了一个脉络清晰的机制来控制资源的生存期。可创建一个对象,该对象在using语句块结束的时候销毁。using语句声明的变量的类型必须实现IDisposable接口。IDisposable接口在System命名空间内,只包含一个名为Dispose的方法。例如:
using(TextReader reader = new StreamReader(filename))
{
string line;
while((line = reader.ReadLine())!=null)
{
Console.WriteLine(line);
}
//等价于:
{
TextReader reader = new StreamReader(filename);
try
{
string line;
while ((line = reader.ReadLine())!=null)
{
Console.WriteLine(line);
}
}
finally //如果前面由异常抛出,不用finally导致下面的代码执行不了,从而文件没关闭
{
if(reader!=null)
{
((IDisposable)reader).Dispose();
}
}
}
}
chapter15 实现属性以访问字段
-
前文强调过,通常将类中的字段设为私有,并提供专门的方法来取值,这样就可以安全地、受控制地访问字段。如果要读写一个私有字段,需要在类方法中分别实现读接口和写接口,太过繁琐,但将字段公开又会破坏封装性。属性,则有两全其美的效果---既维护了封装性,又能使用字段风格的语法。例如:
struct SereePostion
{
private int _x;
Pulblic int X
{
get => this._x;
//所有的set访问器都有一个隐藏的、内建参数value来传递要写入的数据
set => this._x = value;
}
}
-
生成自动属性,c#编译器自动将这个定义转换成私有字段以及一个默认的实现。如下:
class Circle
{
public int Radius{get;set;}
...
}
//等价于
class circle
{
private int _radius;
public int Radius{
get{
return this._radius;
}
set
{
this._radius = value;
}
}
...
}
chapter16 处理二进制数据和使用索引器
-
c#允许用二进制记号法指定整数常量,可以用下划线增加可读性,下划线会被c#编译器忽略。如:
uint testaaa = 0b0_11110000_00001111;//0b0是前缀
//判断一个名为bits的int数值的第6位是0还是1
if(bits & (1 << 5) != 0)
//将bits的第6位设0、设1
//设0
bits &= ~(1 << 5)
//设1
bits |= ~(1 << 5)
-
属性封装类的一个值,索引器封装一组值(使用索引器时,语法和数组相同)。对于上诉的位操作代码,C语言里经常这样写,为了提现C#更好的封装性,可以同索引器代替上诉写法。假定新类型为IntBits,使用this关键字声明索引器,例如:
struct IntBits
{
private int bits;
public Intbits(int InitValue) => bits = InitValue;
public bool this[int index]
{
get => (bits & (1 << index)) != 0;
set
{
if (value)
bits |= (1 << index);
else
bits &= ~(1 << index);
}
}
int adapted = 0b0_01110001;
IntBits bits = new IntBits(adapted);
bool peek = bits[6];//获取索引值为6的bool值
bits[0] = true;//将索引0位置值设为1
}
-
接口可以声明索引器。实现接口的任何类或结构都必须实现接口所声明的索引器的的访问器,在类中实现接口要求的索引器时,可以将索引器的实现声明为virtual,从而允许派生类重写get和set访问器。
chapter17 泛型概述
-
object类和其他普通类都是常规类,该类只有一个实现,它的所有方法获取的都是object类型的参数,返回的也是object类型,用其处理和容纳int、string等其他类型值时都需要进行转化成object类型,或者从object转型为其他类型;泛型则不需要。
-
泛型类和方法接受类型参数,例如:
class Queue<T>
{
private T[] data;
...
public Queue()
{
this.data = new T[SIZE];
}
...
}
-
泛型和约束,约束可以限制泛型类的类型参数实现了一组特定接口,例如:
public class PritableCollection<T> where T : IPrintable
-
书中二叉树的实现这里不再介绍,递归的核心思想是自引用,不属于本书重点关注范畴了。
chapter18 使用集合
-
可以用数组容纳数据,数组很有用,但有很多限制:不方便增大或减小数组大小;不方便对数组中的数据进行排序;访问数组元素必须通过整数索引等。.Net Framework提供了几个类,它们是集合元素,并允许应用程序以特殊方式访问这些元素。
-
分别用数组和集合实现一个先入先出的队列,感受其差别:
using System;
class ArrayQueue
{
private int[] queueArray;
private int front;
private int rear;
private int capacity;
public ArrayQueue(int size)
{
capacity= size;
queueArray = new int[capacity];
front = 0;
rear = -1;
}
public bool IsFull()
{
return rear == capacity - 1;
}
public bool IsEmpty()
{
return rear == -1;
}
public void Enqueue(int item)
{
if (IsFull())
{
throw new Exception("Queue is full");
}
rear++;
queueArray[rear] = item;
}
public int Dequeue()
{
if (IsEmpty())
{
throw new Exception("Queue is empty");
}
int item = queueArray[front];
front++;
if (front > rear)
{
front = 0;
rear = -1;
}
return item;
}
public static void Main(string[] args)
{
ArrayQueue queue = new ArrayQueue(5);
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
Console.WriteLine(queue.Dequeue()); // 输出 1
Console.WriteLine(queue.Dequeue()); // 输出 2
queue.Enqueue(4);
Console.WriteLine(queue.Dequeue()); // 输出 3
}
}
-
C#的System.Collections.Generic命名空间中提供了Queue<T>类,它是一个泛型集合类,内置了先进先出的队列行为。使用Queue<T>实现队列非常简单:
using System;
using System.Collections.Generic;
class QueueDemo
{
public static void Main(string[] args)
{
Queue<int> queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
Console.WriteLine(queue.Dequeue()); // 输出 1
Console.WriteLine(queue.Dequeue()); // 输出 2
queue.Enqueue(4);
Console.WriteLine(queue.Dequeue()); // 输出 3
}
}
-
Lambda表达式,简单来说,Lambda表达式是能返回方法的表达式。Lambda表达式是整个匿名函数的定义,而表达式主体是Lambda表达式中具体执行的那部分代码。例如:
class Program
{
static void Main(string[] args)
{
List<Person> personal = new List<Person>()
{
new Person(){ ID=1,Name="abc"},
new Person(){ ID=2,Name="def"},
};
//Lambda表达式,编译器自己推断参数类型
Person mach = personal.Find((p) => { return p.ID == 2; });
Console.WriteLine($"ID:{mach.ID}\nName:{mach.Name}");
}
struct Person
{
public int ID { get; set; }
public string Name { get; set; }
}
}
chapter19 枚举集合
-
许多.NET框架的集合类,如 List<T>、HashSet<T>、LinkedList<T> 等,都实现了 IEnumerable<T> 接口。这意味着,你可以在不知道具体集合类型的情况下,通过 IEnumerable<T> 引用来操作这些集合。
-
IEnumerable<T> 接口允许你通过 GetEnumerator() 方法获取一个 IEnumerator<T> 对象,后者可以用来逐个访问集合中的元素。这使得你可以使用 foreach 循环来迭代集合中的每个项,而无需知道底层集合的具体实现。
举例说明:
在编程中,"可枚举接口"(Enumerable interface)和"枚举器"(Enumerator)是用于遍历集合或序列中元素的关键概念。它们最常在C#、Java、C++等面向对象的语言中出现。下面分别解释这两个概念,并以C#为例给出示例。
可枚举接口(Enumerable Interface)
可枚举接口定义了一组标准的操作,允许外部代码以一种统一的方式来遍历集合中的元素。在C#中,这个接口是IEnumerable<T>,而在Java中则是Iterable<T>。
using System;
using System.Collections.Generic;
public class MyCollection : IEnumerable<int>
{
private List<int> items = new List<int>();
public MyCollection()
{
for (int i = 0; i < 10; i++)
{ items.Add(i); }
}
public IEnumerator<int> GetEnumerator()
{
return items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class Program
{
static void Main()
{
var collection = new MyCollection();
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
}
枚举器(Enumerator)
枚举器是一个迭代器对象,用于依次访问集合中的每个元素。枚举器实现了IEnumerator<T>接口(在C#中),并且能够跟踪当前的迭代位置。
using System;
using System.Collections;
using System.Collections.Generic;
public class MyEnumerator : IEnumerator<int>
{
private List<int> list; private int position = -1;
public MyEnumerator(List<int> list)
{
this.list = list;
}
public int Current => list[position];
object IEnumerator.Current => Current;
public bool MoveNext()
{
if (position < list.Count - 1)
{
position++; return true;
}
return false;
}
public void Reset()
{
position = -1;
}
public void Dispose()
{
// Implement IDisposable.
}
}
public class MyCollection : IEnumerable<int>
{
private List<int> items = new List<int>();
public MyCollection()
{
for (int i = 0; i < 10; i++)
{
items.Add(i);
}
}
public IEnumerator<int> GetEnumerator()
{
return new MyEnumerator(items);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator(); }
}
class Program
{
static void Main()
{
var collection = new MyCollection();
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
}
在上述C#示例中:
-
MyCollection 类实现了 IEnumerable<int> 接口,这意味着它可以被 foreach 循环遍历。
-
MyEnumerator 类实现了 IEnumerator<int> 接口,它负责实际的迭代逻辑。
-
GetEnumerator() 方法返回一个 MyEnumerator 实例,用于遍历 MyCollection 中的元素。
在实际应用中,通常并不需要手动实现枚举器和可枚举接口,因为大多数集合类(如 List<T> 和 Array)已经内置了这些功能。然而,了解这些底层机制对于编写自定义集合类或更深入理解框架内部工作原理是非常有帮助的。
chapter20 分离应用程序逻辑并处理事件
-
按顺序执行的代码逻辑并不能解决所有问题,为此,“运行时”必须提供两个机制:一个机制通过通知发生了紧急事件,另一个机制规定在发生事件时运行的代码,这正是事件与委托的用途。(类似于嵌入式里的中断与中断函数?)
-
委托是方法的一个集合,感觉可以理解为C语言里的注册函数,调用委托后会自动执行委托里的所有方法。通过关键字delegate声明委托。可以在显示的调用委托,也可以在在事件发生时调用委托来处理。
-
在C#中,委托可以绑定到任何具有与委托定义的签名相匹配的方法。这意味着方法的返回类型和参数列表必须与委托的定义相匹配,但方法本身不需要是相同的。例如,如果委托定义如下,那么,任何接受两个int参数并返回一个int的方法都可以赋值给这个类型的委托(和C语言里容纳函数指针的函数注册表一致):
public delegate int SomeDelegate(int x, int y);
-
声明事件和声明字段类似,但由于事件随委托使用,所以事件类型必须是委托,而且必须在声明前附加event前缀。订阅事件就是将事情触发后的相关方法加到事件委托里。
-
事件可以像引用方法一样通过调用来引用。事情有一个非常有用的内置安全功能:公共事件只能由定义它的那个类中的方法引发。
-
在C#中,事件是一种特殊的委托类型,用于实现观察者模式,允许一个类(事件发布者)通知其他类(事件订阅者)某些状态的变化。举例说明:
定义委托类型
-
首先,你需要定义一个委托类型,这个委托类型将描述事件的签名,包括返回类型和参数列表。
public delegate void NumberChangedEventHandler(int oldValue, int newValue);
声明事件
接着,在事件发布者的类中声明事件,使用event关键字和之前定义的委托类型。
public class Counter
{
// 其他成员...
public event NumberChangedEventHandler ValueChanged;
private int _value;
public int Value
{
get { return _value; }
set
{
if (_value != value)
{
_value = value;
OnValueChanged(_value, value);
}
}
}
protected virtual void OnValueChanged(int oldValue, int newValue)
{
// 这里调用事件,如果有订阅者的话
ValueChanged?.Invoke(oldValue, newValue);
}
}
订阅事件
在事件订阅者的类中,你可以添加事件处理方法,然后将这个方法与事件发布者的事件关联起来。
public class Display
{
public void ShowChange(int oldValue, int newValue)
{
Console.WriteLine($"Counter value changed from {oldValue} to {newValue}");
}
}
调用事件
当事件触发条件满足时,事件发布者会调用事件。在上面的Counter类中,ValueChanged事件在Value属性的setter中被触发。
class Program
{
static void Main(string[] args)
{
Counter counter = new Counter();
Display display = new Display();
// 这里订阅事件
counter.ValueChanged += display.ShowChange;
// 触发事件
counter.Value = 5; // 假设初始值是0,所以这里会触发事件
// 取消订阅
counter.ValueChanged -= display.ShowChange;
// 再次尝试触发事件,但不会有任何输出,因为事件已经被取消订阅
counter.Value = 10;
}
}
在这个例子中:
-
NumberChangedEventHandler是事件的委托类型。
-
Counter类是事件的发布者,它有一个ValueChanged事件。
-
Display类是事件的订阅者,它有一个ShowChange方法来响应事件。
-
在Main方法中,我们订阅了事件,改变了Counter的值,触发了事件,然后取消订阅,并再次改变Counter的值,这次不会触发事件。
chapter21 使用查询表达事来查询内存中的数据
-
LINQ(Language Integrated Query)是C#中的一个强大的查询工具,它提供了一种简洁、一致的方式来查询各种类型的数据源,包括数组、列表、数据库、XML等。LINQ的主要优点在于它能够以接近自然语言的方式表达数据查询逻辑,使得代码更易于理解和维护。
-
LINQ的基本组成部分:
-
查询表达式:类似于SQL的语法,用于描述数据检索和转换的逻辑。
-
标准查询运算符:一组预定义的方法,用于执行常见的数据操作,如筛选、排序、分组等。
-
示例:使用LINQ查询一个整数列表
-
假设我们有一个整数列表,我们想要找到其中所有的偶数,并按降序排序后获取前五个数字。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int>() { 5, 3, 9, 1, 8, 6, 4, 7, 2, 0 };
var result = from n in numbers where n % 2 == 0 // 筛选偶数
orderby n descending // 按降序排序
select n; // 返回数字
// 获取前五个结果
var topFiveEvenNumbers = result.Take(5).ToList();
foreach (var number in topFiveEvenNumbers)
{
Console.WriteLine(number);
}
}
}
-
同样的查询逻辑也可以使用标准查询运算符来实现,代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int>() { 5, 3, 9, 1, 8, 6, 4, 7, 2, 0 };
var topFiveEvenNumbers = numbers.Where(n => n % 2 == 0) // 筛选偶数
.OrderByDescending(n => n) // 按降序排序
.Take(5) // 取前五个
.ToList();
foreach (var number in topFiveEvenNumbers)
{
Console.WriteLine(number);
}
}
}
-
以上两个示例展示了如何使用LINQ来处理数据集合,无论是使用查询表达式还是标准查询运算符,LINQ都提供了强大而灵活的数据处理能力。
charpter22 操作符重载
-
在C#中,操作符重载(Operator Overloading)是一种多态性的体现,它允许程序员改变已有的操作符在自定义类型上的行为。这使得用户定义的类型能够像内置类型一样使用标准操作符,提高代码的可读性和直观性。
-
操作符重载允许你为类或结构体定义如何处理如 +、-、*、/、<、>、== 和 != 等操作符。但是,C#并不允许重载所有操作符,比如赋值操作符 =、条件操作符 && 和 ||、类型查询操作符 is 和 as、指针操作符 ->、数组索引器 [] 等就不支持重载。
-
下面是一个简单的例子,演示如何为一个复数类 Complex 实现加法操作符重载:
public struct Complex
{
public double Real { get; }
public double Imaginary { get; }
public Complex(double real, double imaginary)
{
Real = real; Imaginary = imaginary;
}
// 重载加法操作符
public static Complex operator +(Complex c1, Complex c2)
{
return new Complex(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
}
// 重载等于操作符
public static bool operator ==(Complex c1, Complex c2)
{
return c1.Real == c2.Real && c1.Imaginary == c2.Imaginary;
}
// 重载不等于操作符
public static bool operator !=(Complex c1, Complex c2)
{
return !(c1 == c2);
}
public override string ToString()
{
return $"{Real} + {Imaginary}i";
}
}
// 使用示例
class Program
{
static void Main(string[] args)
{
Complex c1 = new Complex(1, 2);
Complex c2 = new Complex(3, 4);
Complex sum = c1 + c2;
// 使用重载的加法操作符
Console.WriteLine($"Sum: {sum}");
if (c1 == c2)
// 使用重载的等于操作符
{
Console.WriteLine("c1 and c2 are equal.");
}
else
{
Console.WriteLine("c1 and c2 are not equal.");
}
}
}
-
在C#中,类型转换是常见的需求,尤其是当你需要将一个数据类型转换为另一个数据类型时。类型转换有两种主要形式:隐式转换(Implicit Conversion)和显示转换(Explicit Conversion)。
-
隐式转换(Implicit Conversion),隐式转换是由编译器自动进行的类型转换,无需程序员显式指定。这种转换通常发生在将一个较小的数据类型转换为较大的数据类型时,或者从派生类转换到基类时,因为这样不会导致数据丢失。例如:
int number = 10;
long largerNumber = number; // 隐式转换,int 转换为 long
-
显示转换(Explicit Conversion),显示转换需要程序员明确地指定转换操作,通常是因为编译器不能确定转换是安全的,或者转换可能会导致数据丢失。显示转换使用强制类型转换语法,即目标类型前面加上括号 (type)。例如:
double d = 10.5;
int i = (int)d; // 显示转换,double 转换为 int,会丢失小数部分
-
显示转换也可以通过定义转换运算符来实现,这在自定义类中特别有用。你可以为你的类定义 implicit 和 explicit 关键字来控制如何进行类型转换。例如:
public struct MyInteger
{
private int value;
public MyInteger(int val)
{
value = val;
}
// 定义隐式转换
public static implicit operator MyInteger(int val)
{
return new MyInteger(val);
}
// 定义显示转换
public static explicit operator int(MyInteger myInt)
{
return myInt.value;
}
// 其他成员...
}
-
在这个例子中,MyInteger 结构体可以被隐式转换为 int 类型,反之亦然,但后者需要使用 explicit 关键字,因为它可能会导致数据丢失或类型不匹配的问题。
-
总之,隐式转换提高了代码的简洁性,而显示转换提供了更多的控制和安全性,防止了潜在的数据丢失问题。在实际编程中,根据具体情况选择合适的转换方式是很重要的。