C#中面试的常见问题003
1.委托的定义
定义委托
委托可以被视为一个指向方法的引用。你可以定义一个委托类型,该类型指定了方法的签名(即方法的参数列表和返回类型)。以下是定义委托的基本语法:
public delegate int Comparison<T>(T x, T y);
在这个例子中,Comparison<T>
是一个泛型委托,它定义了一个接受两个类型为 T
的参数并返回一个 int
类型的方法。
使用委托
一旦定义了委托类型,就可以创建委托的实例,并将方法赋值给它。这些方法可以是实例方法或静态方法。
public int Compare(int a, int b)
{
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
Comparison<int> comp = new Comparison<int>(Compare);
在这个例子中,Compare
方法被赋值给 Comparison<int>
类型的委托 comp
。
调用委托
委托可以像普通方法一样被调用:
int result = comp(5, 10); // 调用委托,等同于调用 Compare(5, 10)
多播委托
C# 委托支持多播(Multicast),这意味着一个委托可以指向多个方法。你可以使用 +=
和 -=
运算符来添加或移除方法。
public int CompareReverse(int a, int b)
{
return Compare(b, a); // 反向比较
}
comp += CompareReverse; // 添加另一个方法
int result = comp(5, 10); // 两个方法都会被调用
comp -= CompareReverse; // 移除方法
匿名方法和 Lambda 表达式
C# 允许你使用匿名方法或 Lambda 表达式直接创建委托实例,而不需要显式定义方法。
Comparison<int> comp = (a, b) => a.CompareTo(b);
这个 Lambda 表达式创建了一个委托实例,它接受两个 int
参数并返回它们的比较结果。
委托的重要性
委托是C#中实现回调模式的关键,它们使得方法可以作为参数传递给其他方法,这在事件处理、异步编程和LINQ查询中非常有用。
2.常见的委托action和Func有什么区别
Action<T...>
Action
是一个无返回值的委托,可以有零个或多个参数。Action
是从 System.Action
委托派生的泛型委托。
- 无返回值:
Action
委托调用的方法不返回任何值。 - 参数灵活性:
Action
可以定义不同数量和类型的参数。
例如,一个不接受参数且无返回值的方法可以定义为 Action
:
Action myAction = () => Console.WriteLine("Hello, World!");
myAction(); // 输出 "Hello, World!"
或者一个接受两个参数且无返回值的方法:
Action<int, string> myAction = (num, str) => Console.WriteLine(num + " " + str);
myAction(5, "Hello"); // 输出 "5 Hello"
Func<TResult> 和 Func<T, TResult> 等
Func
是一个有返回值的委托,可以有零个或多个参数,并且最后一个类型参数指定返回类型。Func
是从 System.Func
委托派生的泛型委托。
- 有返回值:
Func
委托调用的方法必须返回一个值。 - 参数灵活性:
Func
可以定义不同数量和类型的参数,并且指定返回值的类型。
例如,一个不接受参数且返回值的方法可以定义为 Func
:
Func<int> myFunc = () => 42;
int result = myFunc(); // result 为 42
或者一个接受一个参数且返回值的方法:
Func<int, int> myFunc = x => x * x;
int result = myFunc(5); // result 为 25
总结
- 返回值:
Action
用于无返回值的方法,而Func
用于有返回值的方法。 - 参数:两者都可以接受不同数量的参数,但
Func
需要指定返回值的类型。 - 使用场景:如果你需要一个方法作为参数传递,并且这个方法不需要返回值,那么使用
Action
;如果这个方法需要返回值,那么使用Func
。
3.常用的泛型对象
1. List<T>
List<T>
是一个泛型列表类,提供了一个动态数组的功能,可以存储任何类型的元素。
List<int> intList = new List<int>();
List<string> stringList = new List<string>();
2. Dictionary<TKey, TValue>
Dictionary<TKey, TValue>
是一个泛型字典类,它存储键值对,并且提供了快速的查找功能。
Dictionary<int, string> intToStringDict = new Dictionary<int, string>();
Dictionary<string, int> stringToIntDict = new Dictionary<string, int>();
3. Queue<T>
Queue<T>
是一个泛型队列类,遵循先进先出(FIFO)的原则。
Queue<int> intQueue = new Queue<int>();
Queue<string> stringQueue = new Queue<string>();
4. Stack<T>
Stack<T>
是一个泛型栈类,遵循后进先出(LIFO)的原则。
Stack<int> intStack = new Stack<int>();
Stack<string> stringStack = new Stack<string>();
5. HashSet<T>
HashSet<T>
是一个泛型集合类,存储不重复的元素,并且提供了快速的查找功能。
HashSet<int> intHashSet = new HashSet<int>();
HashSet<string> stringHashSet = new HashSet<string>();
6. SortedList<TKey, TValue>
SortedList<TKey, TValue>
是一个泛型字典类,它存储键值对,并且按键排序。
SortedList<int, string> intToStringSortedList = new SortedList<int, string>();
7. SortedSet<T>
SortedSet<T>
是一个泛型集合类,存储不重复的元素,并且按键排序。
SortedSet<int> intSortedSet = new SortedSet<int>();
8. Tuple<T1, T2, ..., TN>
Tuple
是一个泛型类型,用于将多个值打包成一个单一的复合值。
Tuple<int, string> intStringTuple = new Tuple<int, string>(1, "hello");
9. Nullable<T>
Nullable<T>
是一个泛型结构,它使得值类型可以表示null值。
Nullable<int> nullableInt = null;
10. Generic Collections in System.Collections.Generic
System.Collections.Generic
命名空间提供了一组不依赖于任何特定对象类型的集合类。
11. Delegates like Action<T> and Func<T, TResult>
Action<T>
和 Func<T, TResult>
是泛型委托,用于定义具有特定签名的方法。
Action<string> printAction = (text) => Console.WriteLine(text);
Func<int, string> convertFunc = (number) => number.ToString();
4.泛型约束
1. 类型约束(class)
class
约束指定类型参数必须是引用类型(class类型),不能是值类型。
public class MyClass<T> where T : class
{
// T 必须是引用类型
}
2. 结构体约束(struct)
struct
约束指定类型参数必须是值类型。
public class MyStructClass<T> where T : struct
{
// T 必须是值类型
}
3. 新约束(new())
new()
约束指定类型参数必须有默认构造函数。
public class MyGenericClass<T> where T : new()
{
public T CreateInstance()
{
return new T();
}
}
4. 基类约束
可以指定类型参数必须是特定类的派生类。
public class MyGenericClass<T> where T : MyBaseClass
{
// T 必须是 MyBaseClass 的派生类
}
5. 接口约束
可以指定类型参数必须实现一个或多个接口。
public class MyGenericClass<T> where T : IMyInterface
{
// T 必须实现 IMyInterface 接口
}
public class MyGenericClass<T> where T : IFace1, IFace2
{
// T 必须同时实现 IFace1 和 IFace2 接口
}
6. 多个约束组合
可以组合使用多个约束。
public class MyGenericClass<T> where T : class, IMyInterface, new()
{
// T 必须是引用类型,有默认构造函数,并且实现 IMyInterface 接口
}
7. 类型参数的顺序
当使用多个类型参数时,可以为每个参数单独指定约束。
public class MyDualGenericClass<TFirst, TSecond>
where TFirst : class, IMyInterface
where TSecond : struct, IAnotherInterface
{
// TFirst 必须是引用类型,实现 IMyInterface 接口
// TSecond 必须是值类型,实现 IAnotherInterface 接口
}
5.反射
1. 获取类型信息
反射允许程序在运行时获取类型的信息,包括其成员(字段、属性、方法等)。
Type type = typeof(MyClass);
FieldInfo[] fields = type.GetFields();
PropertyInfo[] properties = type.GetProperties();
MethodInfo[] methods = type.GetMethods();
2. 创建类型实例
使用Activator
类,可以通过反射动态创建类型的实例。
object instance = Activator.CreateInstance(typeof(MyClass));
3. 调用方法和访问成员
反射可以调用类型的公共或私有方法,以及访问字段和属性。
MethodInfo method = type.GetMethod("MethodName");
object result = method.Invoke(instance, new object[] { arg1, arg2 });
4. 动态设置属性值
反射可以用于动态地设置属性值。
PropertyInfo property = type.GetProperty("PropertyName");
property.SetValue(instance, value, null);
5. 泛型类型和方法
反射也支持泛型类型和方法的操作。
Type genericType = typeof(List<>);
Type constructedType = genericType.MakeGenericType(typeof(int));
object genericInstance = Activator.CreateInstance(constructedType);
6. 绑定重载方法
当存在方法重载时,反射可以指定参数类型来绑定到特定的方法。
MethodInfo method = type.GetMethod("MethodName", new Type[] { typeof(int) });
7. 访问非公共成员
反射可以访问私有和受保护的成员,这在单元测试和某些高级编程场景中非常有用。
FieldInfo field = type.GetField("FieldName", BindingFlags.NonPublic | BindingFlags.Instance);
8. 动态语言和脚本
反射是实现动态语言运行时(如Python和Ruby)的基础,它允许这些语言在运行时动态地操作.NET类型。
9. 性能考虑
反射通常比直接代码调用要慢,因为它涉及到运行时类型检查和动态解析。因此,在性能敏感的应用中,应谨慎使用反射。
10. 安全性
反射可以访问程序集中的内部细节,因此使用反射时需要考虑安全性和封装性的问题。
6.反射动态创建对象
1. 获取类型
首先,你需要获取要创建实例的类型的Type
对象。这可以通过几种方式完成:
- 通过
typeof
操作符获取类型的Type
对象。 - 通过程序集加载类型。
- 通过字符串名称获取类型(使用
Type.GetType
或Assembly.GetType
)。
// 使用typeof操作符
Type type = typeof(MyClass);
// 通过类型名称获取
Type typeByName = Type.GetType("MyNamespace.MyClass");
// 通过程序集和类型名称获取
Assembly assembly = Assembly.Load("MyAssembly");
Type typeFromAssembly = assembly.GetType("MyNamespace.MyClass");
2. 使用Activator创建实例
一旦你有了Type
对象,你可以使用Activator
类来创建该类型的实例。Activator.CreateInstance
方法可以无参数调用,也可以传递构造函数参数。
// 无参数创建实例
object instance = Activator.CreateInstance(type);
// 带参数创建实例
object instanceWithParams = Activator.CreateInstance(type, new object[] { param1, param2 });
3. 处理泛型类型
如果需要动态创建泛型类型的实例,可以使用MakeGenericType
方法来创建一个具体化的泛型类型,然后使用Activator.CreateInstance
创建实例。
// 定义泛型类型
Type genericType = typeof(List<>);
// 具体化泛型类型
Type constructedType = genericType.MakeGenericType(typeof(int));
// 创建具体化泛型类型的实例
object genericInstance = Activator.CreateInstance(constructedType);
4. 考虑无参构造函数
Activator.CreateInstance
默认情况下调用无参构造函数。如果类型没有无参构造函数,你需要使用Activator.CreateInstance
的重载版本,该版本允许你指定绑定标志和构造函数参数。
// 指定构造函数参数
ConstructorInfo constructor = type.GetConstructor(new Type[] { typeof(string) });
object instanceWithConstructorParam = Activator.CreateInstance(type, new object[] { "value" });
5. 异常处理
在使用反射创建对象时,可能会遇到各种异常,如TargetException
(如果类型不是类或接口)或MissingMethodException
(如果没有找到匹配的构造函数)。因此,应该适当地处理这些异常。
try
{
object instance = Activator.CreateInstance(type);
}
catch (TargetException te)
{
// 处理异常
}
catch (MissingMethodException mme)
{
// 处理异常
}
7.装箱拆箱
装箱(Boxing)
装箱是将值类型转换为引用类型的过程。当你将一个值类型(如int
、double
、struct
等)赋值给一个object
类型或任何接口类型时,就会发生装箱。装箱操作会在堆上创建一个对象,并复制值类型的数据到堆中,同时返回该对象的引用。
int intValue = 10;
object obj = intValue; // 装箱:将int的值装箱成object
装箱是隐式进行的,开发者不需要显式地进行任何操作。装箱操作会产生额外的内存分配和数据复制,因此可能会影响性能。
拆箱(Unboxing)
拆箱是将引用类型转换回值类型的过程。当你将一个object
类型或任何接口类型的变量赋值给一个值类型时,就会发生拆箱。拆箱操作会从堆中的对象取回数据,并将其复制到栈上。
object obj = 10;
int intValue = (int)obj; // 拆箱:将object的值拆箱成int
拆箱是显式进行的,需要开发者明确地进行类型转换。如果拆箱操作的目标类型与装箱时的原始类型不匹配,就会抛出InvalidCastException
异常。
可空类型(Nullable Types)
C#中的可空类型(Nullable Types)允许值类型存储null
值,它们在装箱时会装箱成null
引用。
int? nullableInt = null;
object obj = nullableInt; // 装箱:将可空的int装箱成object,结果为null
装箱和拆箱的性能考虑
由于装箱和拆箱涉及到内存分配和数据复制,它们可能会对性能产生负面影响,尤其是在性能敏感的代码路径中。因此,如果性能是关键考虑因素,应尽量避免不必要的装箱和拆箱操作。
8.面向对象设计模式
1. 创建型模式(Creational Patterns)
这些模式提供了对象创建机制,同时隐藏创建逻辑,而不是直接使用new运算符实例化对象。
- 单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。
- 工厂方法模式(Factory Method):定义一个创建对象的接口,让子类决定实例化哪一个类。
- 抽象工厂模式(Abstract Factory):创建一系列相关或相互依赖的对象,而不需指定它们具体的类。
- 建造者模式(Builder):构建一个复杂的对象,并允许按步骤构造。
- 原型模式(Prototype):通过复制现有实例创建新对象,而不是通过新建。
2. 结构型模式(Structural Patterns)
这些模式涉及对象的组合,以达到新功能。
- 适配器模式(Adapter):允许对象间的接口不兼容问题,让它们可以一起工作。
- 桥接模式(Bridge):将抽象与实现分离,使它们可以独立变化。
- 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。
- 装饰器模式(Decorator):动态地给一个对象添加额外的职责。
- 外观模式(Facade):为系统中的一组接口提供一个统一的高层接口。
- 享元模式(Flyweight):通过共享来高效地支持大量细粒度的对象。
3. 行为型模式(Behavioral Patterns)
这些模式特别关注对象之间的通信。
- 策略模式(Strategy):定义一系列算法,把它们一个个封装起来,并使它们可以相互替换。
- 模板方法模式(Template Method):在方法中定义一个算法的骨架,而将一些步骤延迟到子类中实现。
- 观察者模式(Observer):对象间的一种一对多的依赖关系,当一个对象改变状态时,所有依赖于它的对象都会得到通知并自动更新。
- 迭代器模式(Iterator):顺序访问一个聚合对象中的各个元素,不暴露其内部的表示。
- 责任链模式(Chain of Responsibility):使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
- 命令模式(Command):将请求封装为一个对象,从而使用户可以使用不同的请求、队列或日志请求。
- 备忘录模式(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 状态模式(State):允许一个对象在其内部状态改变时改变它的行为,看起来好像修改了其类。
- 访问者模式(Visitor):为一个对象结构(如组合结构)增加新能力。
9.工厂模式
工厂模式的类型
工厂模式主要有以下几种类型:
-
简单工厂模式(Simple Factory Pattern)
- 简单工厂模式并不是一个正式的设计模式,而是一种创建对象的简单方法。它通过一个工厂类来创建对象,根据传入的参数决定创建哪个具体类的实例。
public class ShapeFactory { public static IShape GetShape(string shapeType) { switch (shapeType.ToLower()) { case "circle": return new Circle(); case "rectangle": return new Rectangle(); default: throw new ArgumentException("Invalid shape type"); } } }
使用示例:
IShape shape = ShapeFactory.GetShape("circle"); shape.Draw();
-
工厂方法模式(Factory Method Pattern)
- 工厂方法模式定义了一个创建对象的接口,但由子类决定实例化哪个类。工厂方法模式允许类将实例化推迟到子类。
public interface IShape { void Draw(); } public class Circle : IShape { public void Draw() => Console.WriteLine("Drawing a Circle"); } public class Rectangle : IShape { public void Draw() => Console.WriteLine("Drawing a Rectangle"); } public abstract class ShapeFactory { public abstract IShape CreateShape(); } public class CircleFactory : ShapeFactory { public override IShape CreateShape() => new Circle(); } public class RectangleFactory : ShapeFactory { public override IShape CreateShape() => new Rectangle(); }
使用示例:
ShapeFactory factory = new CircleFactory(); IShape shape = factory.CreateShape(); shape.Draw();
-
抽象工厂模式(Abstract Factory Pattern)
- 抽象工厂模式提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们的具体类。它通常用于需要创建多个相关对象的场景。
public interface IShape { void Draw(); } public interface IColor { void Fill(); } public class Circle : IShape { public void Draw() => Console.WriteLine("Drawing a Circle"); } public class Rectangle : IShape { public void Draw() => Console.WriteLine("Drawing a Rectangle"); } public class Red : IColor { public void Fill() => Console.WriteLine("Filling Red"); } public class Blue : IColor { public void Fill() => Console.WriteLine("Filling Blue"); } public interface IAbstractFactory { IShape CreateShape(); IColor CreateColor(); } public class RedShapeFactory : IAbstractFactory { public IShape CreateShape() => new Circle(); public IColor CreateColor() => new Red(); } public class BlueShapeFactory : IAbstractFactory { public IShape CreateShape() => new Rectangle(); public IColor CreateColor() => new Blue(); }
使用示例:
IAbstractFactory factory = new RedShapeFactory(); IShape shape = factory.CreateShape(); IColor color = factory.CreateColor(); shape.Draw(); color.Fill();
工厂模式的优点
- 解耦:客户端代码与具体类解耦,便于替换和扩展。
- 灵活性:可以通过修改工厂类来改变产品的创建逻辑。
- 易于维护:集中管理对象的创建逻辑,便于维护和修改。
工厂模式的缺点
- 复杂性:引入了额外的类和接口,可能会增加系统的复杂性。
- 难以扩展:如果产品种类很多,工厂类可能会变得庞大,难以管理。
10.单例模式的定义
单例模式的特点
- 唯一性:单例模式确保一个类只有一个实例。
- 全局访问:提供一个全局访问点来获取该实例。
- 延迟初始化:可以在需要时创建实例,而不是在应用程序启动时创建。
单例模式的实现
在C#中,单例模式的实现通常包括以下几个步骤:
- 私有构造函数:防止外部代码直接实例化该类。
- 静态变量:用于存储单例的唯一实例。
- 公共静态方法:提供访问实例的全局方法。
以下是单例模式的一个基本实现示例:
public class Singleton
{
// 存储单例的唯一实例
private static Singleton _instance;
// 用于线程安全的锁
private static readonly object _lock = new object();
// 私有构造函数,防止外部实例化
private Singleton()
{
}
// 公共静态方法,提供访问单例的全局方法
public static Singleton Instance
{
get
{
// 双重检查锁定
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
// 示例方法
public void SomeMethod()
{
Console.WriteLine("Executing some method in Singleton.");
}
}
使用示例
使用单例模式时,可以通过Singleton.Instance
访问唯一的实例:
class Program
{
static void Main(string[] args)
{
// 获取单例实例并调用方法
Singleton singleton = Singleton.Instance;
singleton.SomeMethod();
}
}
单例模式的优点
- 控制实例数量:确保一个类只有一个实例,避免资源浪费。
- 全局访问:提供一个全局访问点,方便管理和使用。
- 延迟加载:可以在需要时创建实例,提高性能。
单例模式的缺点
- 并发问题:在多线程环境中,如果没有适当的锁机制,可能会导致多个实例的创建。
- 测试困难:单例模式可能会导致代码的可测试性降低,因为它引入了全局状态。
- 隐藏依赖:使用单例可能会导致类之间的隐式依赖,降低代码的可读性和可维护性。
11.left join和 INNER JOIN 的区别
INNER JOIN(内连接)
INNER JOIN
返回两个表中匹配的记录。只有当连接条件在两个表中都为真时,结果集中才会包含一行。
- 结果集:返回两个表中连接条件相匹配的行。
- 行为:如果左表(第一个表)的一行在右表(第二个表)中没有匹配的行,则这行不会出现在结果集中。
- 示例:
这个查询将返回SELECT a.*, b.* FROM TableA a INNER JOIN TableB b ON a.Key = b.Key;
TableA
和TableB
中Key
列相匹配的所有行。
LEFT JOIN(左连接)
LEFT JOIN
返回左表(第一个表)的所有记录,即使右表(第二个表)中没有匹配的记录。如果左表的一行在右表中没有匹配的行,则结果集中这些行的右表字段将为NULL。
- 结果集:返回左表中的所有行,如果右表中有匹配的行,则返回匹配的行,否则返回NULL。
- 行为:左表的所有行都会出现在结果集中,右表的行只会在连接条件为真时出现。
- 示例:
这个查询将返回SELECT a.*, b.* FROM TableA a LEFT JOIN TableB b ON a.Key = b.Key;
TableA
中的所有行,如果TableB
中有Key
列相匹配的行,则同时返回这些行,否则TableB
中的字段将为NULL。
区别总结
- 结果集不同:
INNER JOIN
只返回匹配的行,而LEFT JOIN
返回左表的所有行,包括那些在右表中没有匹配的行。 - 行为不同:
INNER JOIN
要求两个表中都有匹配的行,LEFT JOIN
则不需要右表中有匹配的行。 - 使用场景:如果你需要两个表中都有的数据,使用
INNER JOIN
;如果你需要左表的所有数据,无论右表中是否有匹配的数据,使用LEFT JOIN
。
12.TCP编程
基本概念
- 套接字(Socket):网络通信的端点,是TCP编程中的基本构建块。
- IP地址和端口号:用于标识网络中的唯一设备和特定服务。
- 服务器(Server):等待客户端连接的计算机。
- 客户端(Client):发起连接请求的计算机。
- 三次握手(Three-way Handshake):建立TCP连接的过程。
- 四次挥手(Four-way Wavehand):终止TCP连接的过程。
TCP服务器编程步骤
-
创建套接字:服务器和客户端都需要创建一个套接字。
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
-
绑定套接字:服务器需要将套接字绑定到一个IP地址和端口号。
serverSocket.Bind(new IPEndPoint(IPAddress.Any, portNumber));
-
监听连接:服务器开始监听进入的连接请求。
serverSocket.Listen(backlog);
-
接受连接:服务器接受客户端的连接请求,创建一个新的套接字用于与客户端通信。
Socket clientSocket = serverSocket.Accept();
-
数据传输:使用
Send
和Receive
方法进行数据传输。byte[] buffer = new byte[bufferSize]; int bytesRead = clientSocket.Receive(buffer);
-
关闭连接:完成通信后关闭套接字。
clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close();
TCP客户端编程步骤
-
创建套接字:同服务器。
-
连接服务器:客户端使用服务器的IP地址和端口号发起连接请求。
clientSocket.Connect(serverEndPoint);
-
数据传输:同服务器。
-
关闭连接:同服务器。
示例代码
以下是C#中TCP服务器和客户端的简单示例:
服务器代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class TcpServer
{
public static void Start(int port)
{
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
serverSocket.Listen(100);
Console.WriteLine("Server started. Waiting for connections...");
while (true)
{
Socket clientSocket = serverSocket.Accept();
Console.WriteLine("Client connected.");
byte[] buffer = new byte[1024];
int bytesRead = clientSocket.Receive(buffer);
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: " + message);
clientSocket.Send(Encoding.UTF8.GetBytes("Hello from server!"));
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
}
}
客户端代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class TcpClient
{
public static void Connect(string ip, int port)
{
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.Connect(ip, port);
Console.WriteLine("Connected to server.");
clientSocket.Send(Encoding.UTF8.GetBytes("Hello from client!"));
byte[] buffer = new byte[1024];
int bytesRead = clientSocket.Receive(buffer);
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: " + message);
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
}
注意事项
- 异常处理:在实际应用中,需要添加适当的异常处理代码。
- 资源管理:确保及时释放套接字资源。
- 安全性:考虑使用SSL/TLS等加密协议保护数据传输的安全。
- 性能优化:对于高并发场景,考虑使用异步IO或多线程/多进程模型。
13.粘包
粘包的原因
TCP是一个面向流的协议,它不保留数据包边界信息。TCP将数据视为字节流,当发送方发送多个数据包时,接收方可能无法确定这些数据包的边界,因为TCP层可能会将这些数据包合并在一起传输,或者在接收方的TCP缓冲区中合并。这种现象就是所谓的“粘包”。
粘包的影响
粘包问题可能导致接收方无法正确解析数据,因为接收方无法确定每个数据包的开始和结束位置。这在处理需要精确数据边界的应用层协议时尤为重要,比如HTTP请求、JSON消息等。
解决粘包的方法
-
固定长度消息:如果每个消息都有固定的长度,接收方可以简单地按照这个长度来读取和解析消息。
-
消息分隔符:在每个消息的末尾添加一个特殊的分隔符,如换行符
\n
或特定的序列,这样接收方可以通过查找分隔符来确定消息的边界。 -
长度前缀:在每个消息的开始处添加一个长度字段,指明消息的长度。这样接收方可以先读取长度字段,然后根据这个长度来读取后续的消息内容。
-
消息终止:发送方在发送完一个消息后,发送一个特殊的终止字符或标志位,表明消息的结束。
-
应用层协议:使用具有内置消息边界处理机制的应用层协议,如HTTP、FTP等。
-
TCP心跳包:定期发送心跳包,以确保TCP连接的活跃性,减少粘包的可能性。
-
调整TCP缓冲区大小:通过调整TCP缓冲区的大小,可以减少粘包发生的机会,但这通常需要操作系统级别的配置。
-
使用消息队列:在应用层实现消息队列,确保消息的顺序和完整性。
示例:长度前缀法
假设我们使用长度前缀法来解决粘包问题,以下是可能的实现:
// 发送方
public void Send(string message)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
byte[] lengthBytes = BitConverter.GetBytes(messageBytes.Length);
byte[] fullMessage = new byte[lengthBytes.Length + messageBytes.Length];
Buffer.BlockCopy(lengthBytes, 0, fullMessage, 0, lengthBytes.Length);
Buffer.BlockCopy(messageBytes, 0, fullMessage, lengthBytes.Length, messageBytes.Length);
socket.Send(fullMessage);
}
// 接收方
public string Receive()
{
byte[] lengthBytes = new byte[4];
socket.Receive(lengthBytes);
int messageLength = BitConverter.ToInt32(lengthBytes, 0);
byte[] messageBytes = new byte[messageLength];
socket.Receive(messageBytes);
return Encoding.UTF8.GetString(messageBytes);
}
在这个例子中,发送方在发送消息前先发送一个4字节的长度字段,然后发送实际的消息内容。接收方首先接收长度字段,然后根据这个长度接收实际的消息内容。
14.三次握手四次挥手
三次握手(Three-way Handshake)
三次握手是建立TCP连接的过程,确保双方都准备好进行数据传输。具体步骤如下:
-
SYN:客户端发送一个SYN(同步)包到服务器,表示请求建立连接。此包中包含客户端的初始序列号(ISN)。
Client -> Server: SYN, Seq = x
-
SYN-ACK:服务器收到SYN包后,回复一个SYN-ACK包,表示同意建立连接。此包中包含服务器的初始序列号,并确认客户端的序列号。
Server -> Client: SYN-ACK, Seq = y, Ack = x + 1
-
ACK:客户端收到SYN-ACK包后,发送一个ACK(确认)包给服务器,确认连接建立。
Client -> Server: ACK, Seq = x + 1, Ack = y + 1
完成这三次握手后,TCP连接建立成功,双方可以开始数据传输。
四次挥手(Four-way Handshake)
四次挥手是终止TCP连接的过程,确保双方都能正常关闭连接。具体步骤如下:
-
FIN:主动关闭连接的一方(通常是客户端)发送一个FIN(结束)包,表示它已经完成数据发送。
Client -> Server: FIN, Seq = x
-
ACK:服务器收到FIN包后,发送一个ACK包,确认收到FIN包。
Server -> Client: ACK, Seq = y, Ack = x + 1
-
FIN:服务器准备关闭连接时,发送一个FIN包给客户端,表示服务器也完成了数据发送。
Server -> Client: FIN, Seq = y, Ack = x + 1
-
ACK:客户端收到服务器的FIN包后,发送一个ACK包,确认收到FIN包。
Client -> Server: ACK, Seq = x + 1, Ack = y + 1
完成这四次挥手后,TCP连接正式关闭,双方都可以释放资源。
总结
- 三次握手:用于建立TCP连接,确保双方都准备好进行数据传输。
- 四次挥手:用于终止TCP连接,确保双方都能正常关闭连接。
15.数据解析
1. JSON解析
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。
- 在C#中解析JSON:
using Newtonsoft.Json; var jsonString = "{\"name\":\"John\", \"age\":30}"; var obj = JsonConvert.DeserializeObject<Dictionary<string, object>>(jsonString); string name = (string)obj["name"]; int age = (int)obj["age"];
2. XML解析
XML(eXtensible Markup Language)是一种标记语言,用于存储和传输数据。
- 在C#中解析XML:
using System.Xml; var xmlString = "<person><name>John</name><age>30</age></person>"; XmlDocument doc = new XmlDocument(); doc.LoadXml(xmlString); XmlNode nameNode = doc.SelectSingleNode("//person/name"); XmlNode ageNode = doc.SelectSingleNode("//person/age"); string name = nameNode.InnerText; int age = int.Parse(ageNode.InnerText);
3. CSV解析
CSV(Comma-Separated Values)是一种简单的文件格式,用于存储表格数据。
- 在C#中解析CSV:
var csvData = "name,age\nJohn,30\nDoe,25"; var lines = csvData.Split('\n'); foreach (var line in lines) { var values = line.Split(','); string name = values[0]; int age = int.Parse(values[1]); // Process the data }
4. 二进制数据解析
二进制数据通常来自于文件或网络传输,需要根据数据结构手动解析。
- 在C#中解析二进制数据:
var binaryData = new byte[] { 0x01, 0x00, 0x00, 0x00, 0x7A, 0x68 }; // Example binary data using (var memoryStream = new MemoryStream(binaryData)) using (var binaryReader = new BinaryReader(memoryStream)) { int firstInt = binaryReader.ReadInt32(); // Read an integer string str = binaryReader.ReadString(); // Read a string // Process the data }
5. 正则表达式解析
正则表达式用于匹配和解析文本模式。
- 在C#中使用正则表达式:
var text = "John Doe <johndoe@example.com>"; var match = Regex.Match(text, @"(\w+) \w+ <(\w+)@\w+\.\w+>"); if (match.Success) { string firstName = match.Groups[1].Value; string email = match.Groups[2].Value; // Process the data }
6. 自定义协议解析
对于自定义的数据格式或协议,需要根据协议规范手动解析数据。
- 解析步骤:
- 确定数据格式和结构。
- 读取数据流。
- 根据结构解析数据字段。
- 处理解析后的数据。
16.序列化和序列化
序列化(Serialization)
序列化是将对象的状态信息转换为可以存储或传输的格式(如XML、JSON、二进制等)的过程。序列化后的数据显示为字节流,可以保存到文件、数据库或通过网络传输。
序列化的目的:
- 持久化:将对象状态保存到文件或数据库中。
- 网络传输:在网络上发送对象状态。
- 跨平台数据交换:在不同的操作系统或平台之间交换数据。
序列化的类型:
- 文本序列化:如JSON和XML,易于阅读和调试,但可能体积较大。
- 二进制序列化:如二进制格式,体积较小,读写速度快,但不如文本格式易于阅读。
C#中序列化的示例(JSON):
using Newtonsoft.Json;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
var person = new Person { Name = "John", Age = 30 };
string json = JsonConvert.SerializeObject(person);
反序列化(Deserialization)
反序列化是将序列化后的格式(如JSON、XML、二进制等)转换回对象状态的过程。反序列化允许从存储介质或网络接收的数据重新构建对象。
反序列化的目的:
- 数据恢复:从文件或数据库中恢复对象状态。
- 数据接收:接收网络上传输的对象状态。
- 跨平台数据使用:在不同的操作系统或平台中使用交换的数据。
C#中反序列化的示例(JSON):
using Newtonsoft.Json;
string json = "{\"Name\":\"John\",\"Age\":30}";
Person person = JsonConvert.DeserializeObject<Person>(json);
注意事项
- 性能:序列化和反序列化可能会影响性能,尤其是在处理大量数据时。
- 安全性:序列化和反序列化时要注意安全问题,避免序列化敏感数据或反序列化不可信的数据。
- 版本兼容性:在软件版本更新时,需要考虑序列化格式的兼容性问题。
- 数据完整性:确保序列化和反序列化过程中数据的完整性和一致性。
17.多线程的创建
1. 使用Thread
类
System.Threading.Thread
类是最基础的多线程机制,允许你创建一个新线程来执行一个方法。
// 使用Thread类创建线程
Thread thread = new Thread(new ThreadStart(MyMethod));
thread.Start(); // 启动线程
void MyMethod()
{
// 线程要执行的代码
}
2. 使用Task
类
System.Threading.Tasks.Task
提供了一种更现代的异步编程模型,它比传统的线程更容易使用和控制。
// 使用Task类创建线程
Task task = Task.Run(() => MyMethod());
void MyMethod()
{
// 线程要执行的代码
}
3. 使用ThreadPool
System.Threading.ThreadPool
是一个线程池,用于管理和重用线程,适合执行短期、异步的任务。
// 使用ThreadPool执行任务
ThreadPool.QueueUserWorkItem(new WaitCallback(MyMethod));
void MyMethod(object state)
{
// 线程要执行的代码
}
4. 使用async
和await
C# 5.0引入了async
和await
关键字,使得异步编程更加简洁和易于管理。
// 使用async和await创建异步任务
public async Task MyAsyncMethod()
{
await Task.Run(() => MyMethod());
}
void MyMethod()
{
// 线程要执行的代码
}
5. 使用Parallel
类
System.Threading.Tasks.Parallel
类用于并行执行任务,可以简化多线程编程模型。
// 使用Parallel类并行执行循环
Parallel.For(0, 10, i =>
{
// 每个i的值将在不同的线程上执行
Console.WriteLine($"Value: {i}");
});
6. 使用ThreadLocal<T>
System.Threading.ThreadLocal<T>
允许每个线程存储自己的数据副本,这对于需要线程特定数据的情况非常有用。
ThreadLocal<int> localData = new ThreadLocal<int>(() => 0);
void MyMethod()
{
int data = localData.Value; // 每个线程都有自己的数据副本
}
注意事项
- 线程安全:在多线程环境中,确保对共享资源的访问是线程安全的。
- 死锁:避免死锁,确保线程在等待资源时能够及时获得。
- 资源管理:合理管理线程资源,避免创建过多的线程导致资源耗尽。
- 异常处理:适当处理线程中的异常,避免线程异常导致程序崩溃。
18.task和线程池的区别
-
抽象层次:
Task
是任务并行库(TPL)的一部分,它在线程池之上提供了一个更高层次的抽象。Task
对象封装了异步操作,使得开发者可以更容易地管理异步代码和处理操作结果。- 线程池是.NET提供的低层次服务,用于管理和重用线程,减少创建和销毁线程的开销。
-
使用方式:
Task
提供了更简洁的API来创建和管理异步操作。例如,Task.Run
是一个快捷方式,用于在线程池线程上执行代码。- 线程池通常通过
ThreadPool.QueueUserWorkItem
方法来排队工作项,这些工作项将在线程池的线程上异步执行。
-
任务调度:
Task
可以在同一个线程中顺序执行多个任务,减少上下文切换,而线程池则可能在不同的线程之间调度任务。Task
提供了更好的任务调度和错误处理机制,以及对任务完成的响应(如ContinueWith
)。
-
返回值和状态:
Task
可以很容易地处理返回值和任务状态,例如通过Task.Result
属性获取任务结果。- 线程池的工作项通常不直接返回值,需要通过其他机制(如回调或共享状态)来处理结果。
-
线程创建和管理:
Task
创建的是线程池任务,而Thread
默认创建的是前台线程。Task
在内部使用了线程池,但提供了更细粒度的控制。- 线程池的工作项通常只运行执行时间较短的异步操作,而
Task
可以用于长时间运行的任务,尽管这可能涉及到创建新的线程。
-
性能优化:
Task
对线程池进行了优化,例如在同一个线程中顺序执行多个任务,减少任务上下文切换带来的时间浪费。
-
取消和异常处理:
Task
提供了更丰富的取消和异常处理机制,例如使用CancellationToken
来取消任务。
19.线程和多线程的同步
1. 锁(Locks)
锁是最基本的同步机制之一,它允许一个线程在执行代码块时阻止其他线程进入。
private readonly object _lockObject = new object();
public void ThreadSafeMethod()
{
lock (_lockObject)
{
// 访问或修改共享资源
}
}
2. Monitor
Monitor
类提供了一种进入和退出同步代码块的方法,类似于锁。
private readonly object _monitor = new object();
public void ThreadSafeMethod()
{
Monitor.Enter(_monitor);
try
{
// 访问或修改共享资源
}
finally
{
Monitor.Exit(_monitor);
}
}
3. Mutex(互斥锁)
Mutex
是一种跨进程的同步机制,但也可以在同一个进程的不同线程之间使用。
private static Mutex _mutex = new Mutex();
public void ThreadSafeMethod()
{
_mutex.WaitOne();
try
{
// 访问或修改共享资源
}
finally
{
_mutex.ReleaseMutex();
}
}
4. Semaphore(信号量)
Semaphore
用于控制对特定资源的并发访问数量。
private static Semaphore _semaphore = new Semaphore(1, 1);
public void ThreadSafeMethod()
{
_semaphore.WaitOne();
try
{
// 访问或修改共享资源
}
finally
{
_semaphore.Release();
}
}
5. AutoResetEvent 和 ManualResetEvent
这些事件等待句柄用于线程间的信号传递。
private static AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
public void SignalMethod()
{
// 发送信号
_autoResetEvent.Set();
}
public void WaitMethod()
{
// 等待信号
_autoResetEvent.WaitOne();
}
6. ReaderWriterLockSlim
ReaderWriterLockSlim
提供了对读者-写者锁的更细粒度控制。
private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
public void ReadData()
{
_rwLock.EnterReadLock();
try
{
// 读取数据
}
finally
{
_rwLock.ExitReadLock();
}
}
public void WriteData()
{
_rwLock.EnterWriteLock();
try
{
// 写入数据
}
finally
{
_rwLock.ExitWriteLock();
}
}
7. volatile 关键字
volatile
关键字用于修饰字段,确保对该字段的读写操作都是原子的,并且立即更新到主内存。
private volatile bool _flag;
public void SetFlag()
{
_flag = true;
}
public void CheckFlag()
{
if (_flag)
{
// 执行操作
}
}
8. Interlocked 类
Interlocked
类提供了一系列静态方法,用于执行原子操作,如递增、递减、交换和比较交换。
private int _counter;
public void IncrementCounter()
{
Interlocked.Increment(ref _counter);
}
注意事项
- 避免死锁:确保在所有情况下都能释放锁,避免嵌套锁顺序不一致。
- 避免活锁:确保线程在等待锁时能够响应外部条件变化。
- 避免资源竞争:尽量减少锁的持有时间,只在必要时锁定共享资源。
20.跨线程访问winForm
1. Invoke 方法
Control.Invoke
方法用于在控件的创建线程(通常是主UI线程)上执行指定的委托。
// 在工作线程中
myControl.Invoke((MethodInvoker)delegate
{
// 直接访问控件,例如:
myTextBox.Text = "更新文本";
});
2. BeginInvoke 和 EndInvoke 方法
如果你不需要等待操作完成,可以使用 BeginInvoke
来异步执行委托,并通过 EndInvoke
等待操作完成。
// 在工作线程中
IAsyncResult result = myControl.BeginInvoke((MethodInvoker)delegate
{
// 直接访问控件,例如:
myTextBox.Text = "更新文本";
});
// 等待操作完成
myControl.EndInvoke(result);
3. 使用 BackgroundWorker
BackgroundWorker
组件提供了一种简单的方式来在后台线程上执行操作,并安全地更新UI。
// 设置 BackgroundWorker
backgroundWorker1.DoWork += (sender, e) =>
{
// 后台线程中执行的工作
};
backgroundWorker1.RunWorkerCompleted += (sender, e) =>
{
// 安全地更新UI
myTextBox.Text = "操作完成";
};
// 启动后台工作线程
backgroundWorker1.RunWorkerAsync();
4. Control.InvokeRequired 属性
Control.InvokeRequired
属性指示是否需要使用 Invoke
来跨线程访问控件。
if (myControl.InvokeRequired)
{
myControl.Invoke((MethodInvoker)delegate
{
// 安全地更新UI
myTextBox.Text = "更新文本";
});
}
else
{
// 直接更新UI,因为我们在UI线程上
myTextBox.Text = "更新文本";
}
注意事项
- 避免在非UI线程上直接访问UI控件,这可能会导致应用程序崩溃或不稳定。
- 使用
Invoke
或BeginInvoke
时,委托执行可能会被排队,如果主线程忙,委托可能不会立即执行。 BackgroundWorker
提供了一种更高级的异步模式,允许你在操作完成时更新UI,并处理取消操作和进度报告。
21.Invoke和begin Invoke区别
Invoke 方法
Invoke
方法是同步的,它将指定的委托排队到控件的创建线程(通常是主UI线程)上执行,并阻塞调用线程直到委托执行完成。这意味着在委托执行期间,调用线程不能做其他工作,它必须等待委托在UI线程上执行完毕。
// 调用线程将被阻塞,直到委托执行完毕
myControl.Invoke((MethodInvoker)delegate
{
// 这段代码将在UI线程上执行,调用线程将等待其完成
myControl.Text = "更新UI";
});
BeginInvoke 方法
BeginInvoke
方法是异步的,它将指定的委托排队到控件的创建线程上执行,但不会阻塞调用线程。调用线程可以继续执行,而委托将在UI线程上异步执行。
// 调用线程不会被阻塞,委托将在UI线程上异步执行
IAsyncResult asyncResult = myControl.BeginInvoke((MethodInvoker)delegate
{
// 这段代码将在UI线程上执行,但调用线程将继续执行
myControl.Text = "更新UI";
});
// 可以在需要的时候检查委托是否完成
// 例如,使用 EndInvoke 方法等待委托执行完毕
myControl.EndInvoke(asyncResult);
主要区别
- 同步 vs 异步:
Invoke
是同步方法,会阻塞调用线程;BeginInvoke
是异步方法,不会阻塞调用线程。 - 返回类型:
Invoke
返回委托执行的结果(如果有),而BeginInvoke
返回一个IAsyncResult
对象,可以用来查询委托的状态或在委托完成时获取结果。 - 控制流:使用
Invoke
时,调用线程的执行流会被暂停,直到委托在UI线程上执行完毕;使用BeginInvoke
时,调用线程可以继续执行其他任务。
使用场景
- 当你需要立即更新UI并且不介意阻塞当前线程时,可以使用
Invoke
。 - 当你需要执行UI更新而不阻塞当前线程时,可以使用
BeginInvoke
,这在处理长时间运行的任务或需要保持响应性的场景中非常有用。
22.B/S
B/S(Browser/Server,浏览器/服务器)架构是一种网络应用模型,它将用户界面完全基于Web浏览器实现,后端逻辑和数据库处理则由服务器端处理。这种架构模式使得用户可以通过互联网访问应用程序,而无需在本地安装特定的客户端软件。以下是B/S架构的一些关键特点和组件:
特点
- 跨平台性:用户可以通过任何支持Web浏览器的设备访问应用程序,不受操作系统限制。
- 集中式管理:应用程序的更新和维护都在服务器端进行,简化了管理流程。
- 可扩展性:服务器可以根据需求进行扩展,以支持更多的用户和更高的负载。
- 成本效益:用户不需要安装和维护客户端软件,降低了总体拥有成本。
- 安全性:安全策略可以在服务器端集中实施,如使用SSL/TLS加密通信。
组件
-
客户端(Browser):
- 用户通过Web浏览器访问应用程序。
- 浏览器作为客户端,负责显示内容、收集用户输入和发送请求到服务器。
-
服务器(Server):
- 处理业务逻辑、数据存储和检索。
- 可以进一步细分为应用服务器和数据库服务器。
- 应用服务器处理应用程序逻辑,数据库服务器管理数据存储。
-
网络:
- 互联网作为客户端和服务器之间的通信媒介。
- 使用HTTP/HTTPS等协议进行数据传输。
-
数据库:
- 存储应用程序的数据。
- 可以是关系型数据库(如MySQL、PostgreSQL)或非关系型数据库(如MongoDB)。
工作流程
- 用户通过浏览器发送请求到服务器。
- 服务器接收请求,处理业务逻辑。
- 服务器从数据库检索或存储数据。
- 服务器将处理结果(通常是HTML页面、JSON数据或其他资源)返回给浏览器。
- 浏览器渲染结果,展示给用户。
技术栈
- 前端技术:HTML、CSS、JavaScript、框架(如React、Angular、Vue.js)。
- 后端技术:服务器端编程语言(如Java、.NET、PHP、Python)、Web框架(如Spring、Django、Express.js)。
- 数据库技术:MySQL、PostgreSQL、MongoDB、SQL Server等。
23.缓存数据库的常用的数据类型
缓存数据库是用来存储频繁访问的数据以提高性能的系统。在缓存数据库中,常用的数据类型包括:
-
字符串(String):
- 用于存储文本数据,如用户信息、配置设置等。
-
整数(Integer):
- 用于存储整数值,如计数器、状态码等。
-
浮点数(Float/Double):
- 用于存储小数点数值,如价格、计算结果等。
-
布尔值(Boolean):
- 用于存储真/假值,如标志位、开关状态等。
-
列表(List):
- 用于存储有序集合,如用户列表、权限列表等。
-
集合(Set):
- 用于存储无序且不重复的元素集合,如唯一标识符集合等。
-
有序集合(Sorted Set):
- 用于存储带有分数(或权重)的元素集合,元素可以按分数排序,如排行榜等。
-
哈希表(Hash):
- 用于存储键值对集合,如对象属性的缓存等。
-
二进制数据(Binary Data):
- 用于存储图片、文件等二进制数据。
-
时间戳(Timestamp):
- 用于记录时间信息,如最后访问时间、过期时间等。
-
地理空间数据类型(Geospatial Data Types):
- 用于存储地理位置信息,如经纬度等,支持地理空间查询。
-
超日志(HyperLogLog):
- 用于基数统计,估计一个集合中不同元素的数量,占用空间小,适用于大数据量。
-
位图(Bitmap):
- 用于位操作,可以用于实现布隆过滤器等。
-
JSON/其他序列化对象:
- 用于存储复杂结构的数据,如JSON对象等。
-
游标(Cursor):
- 用于在迭代大量数据时保持状态,如分页查询等。