欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 维修 > C#中面试的常见问题003

C#中面试的常见问题003

2024/11/30 14:43:56 来源:https://blog.csdn.net/weixin_64532720/article/details/144063255  浏览:    关键词: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, IMyInterfacewhere 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.GetTypeAssembly.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)

装箱是将值类型转换为引用类型的过程。当你将一个值类型(如intdoublestruct等)赋值给一个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.工厂模式

工厂模式的类型

工厂模式主要有以下几种类型:

  1. 简单工厂模式(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();
  2. 工厂方法模式(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();
  3. 抽象工厂模式(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.单例模式的定义

单例模式的特点

  1. 唯一性:单例模式确保一个类只有一个实例。
  2. 全局访问:提供一个全局访问点来获取该实例。
  3. 延迟初始化:可以在需要时创建实例,而不是在应用程序启动时创建。

单例模式的实现

在C#中,单例模式的实现通常包括以下几个步骤:

  1. 私有构造函数:防止外部代码直接实例化该类。
  2. 静态变量:用于存储单例的唯一实例。
  3. 公共静态方法:提供访问实例的全局方法。

以下是单例模式的一个基本实现示例:

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();}
}

单例模式的优点

  1. 控制实例数量:确保一个类只有一个实例,避免资源浪费。
  2. 全局访问:提供一个全局访问点,方便管理和使用。
  3. 延迟加载:可以在需要时创建实例,提高性能。

单例模式的缺点

  1. 并发问题:在多线程环境中,如果没有适当的锁机制,可能会导致多个实例的创建。
  2. 测试困难:单例模式可能会导致代码的可测试性降低,因为它引入了全局状态。
  3. 隐藏依赖:使用单例可能会导致类之间的隐式依赖,降低代码的可读性和可维护性。

11.left join和 INNER JOIN 的区别

INNER JOIN(内连接)

INNER JOIN返回两个表中匹配的记录。只有当连接条件在两个表中都为真时,结果集中才会包含一行。

  • 结果集:返回两个表中连接条件相匹配的行。
  • 行为:如果左表(第一个表)的一行在右表(第二个表)中没有匹配的行,则这行不会出现在结果集中。
  • 示例
    SELECT a.*, b.* 
    FROM TableA a
    INNER JOIN TableB b ON a.Key = b.Key;
    这个查询将返回TableATableBKey列相匹配的所有行。

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编程

基本概念

  1. 套接字(Socket):网络通信的端点,是TCP编程中的基本构建块。
  2. IP地址和端口号:用于标识网络中的唯一设备和特定服务。
  3. 服务器(Server):等待客户端连接的计算机。
  4. 客户端(Client):发起连接请求的计算机。
  5. 三次握手(Three-way Handshake):建立TCP连接的过程。
  6. 四次挥手(Four-way Wavehand):终止TCP连接的过程。

TCP服务器编程步骤

  1. 创建套接字:服务器和客户端都需要创建一个套接字。

    Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  2. 绑定套接字:服务器需要将套接字绑定到一个IP地址和端口号。

    serverSocket.Bind(new IPEndPoint(IPAddress.Any, portNumber));
  3. 监听连接:服务器开始监听进入的连接请求。

    serverSocket.Listen(backlog);
  4. 接受连接:服务器接受客户端的连接请求,创建一个新的套接字用于与客户端通信。

    Socket clientSocket = serverSocket.Accept();
  5. 数据传输:使用SendReceive方法进行数据传输。

    byte[] buffer = new byte[bufferSize];
    int bytesRead = clientSocket.Receive(buffer);
  6. 关闭连接:完成通信后关闭套接字。

    clientSocket.Shutdown(SocketShutdown.Both);
    clientSocket.Close();

TCP客户端编程步骤

  1. 创建套接字:同服务器。

  2. 连接服务器:客户端使用服务器的IP地址和端口号发起连接请求。

    clientSocket.Connect(serverEndPoint);
  3. 数据传输:同服务器。

  4. 关闭连接:同服务器。

示例代码

以下是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();}
}

注意事项

  1. 异常处理:在实际应用中,需要添加适当的异常处理代码。
  2. 资源管理:确保及时释放套接字资源。
  3. 安全性:考虑使用SSL/TLS等加密协议保护数据传输的安全。
  4. 性能优化:对于高并发场景,考虑使用异步IO或多线程/多进程模型。

13.粘包

粘包的原因

TCP是一个面向流的协议,它不保留数据包边界信息。TCP将数据视为字节流,当发送方发送多个数据包时,接收方可能无法确定这些数据包的边界,因为TCP层可能会将这些数据包合并在一起传输,或者在接收方的TCP缓冲区中合并。这种现象就是所谓的“粘包”。

粘包的影响

粘包问题可能导致接收方无法正确解析数据,因为接收方无法确定每个数据包的开始和结束位置。这在处理需要精确数据边界的应用层协议时尤为重要,比如HTTP请求、JSON消息等。

解决粘包的方法

  1. 固定长度消息:如果每个消息都有固定的长度,接收方可以简单地按照这个长度来读取和解析消息。

  2. 消息分隔符:在每个消息的末尾添加一个特殊的分隔符,如换行符\n或特定的序列,这样接收方可以通过查找分隔符来确定消息的边界。

  3. 长度前缀:在每个消息的开始处添加一个长度字段,指明消息的长度。这样接收方可以先读取长度字段,然后根据这个长度来读取后续的消息内容。

  4. 消息终止:发送方在发送完一个消息后,发送一个特殊的终止字符或标志位,表明消息的结束。

  5. 应用层协议:使用具有内置消息边界处理机制的应用层协议,如HTTP、FTP等。

  6. TCP心跳包:定期发送心跳包,以确保TCP连接的活跃性,减少粘包的可能性。

  7. 调整TCP缓冲区大小:通过调整TCP缓冲区的大小,可以减少粘包发生的机会,但这通常需要操作系统级别的配置。

  8. 使用消息队列:在应用层实现消息队列,确保消息的顺序和完整性。

示例:长度前缀法

假设我们使用长度前缀法来解决粘包问题,以下是可能的实现:

// 发送方
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连接的过程,确保双方都准备好进行数据传输。具体步骤如下:

  1. SYN:客户端发送一个SYN(同步)包到服务器,表示请求建立连接。此包中包含客户端的初始序列号(ISN)。

    Client -> Server: SYN, Seq = x
  2. SYN-ACK:服务器收到SYN包后,回复一个SYN-ACK包,表示同意建立连接。此包中包含服务器的初始序列号,并确认客户端的序列号。

    Server -> Client: SYN-ACK, Seq = y, Ack = x + 1
  3. ACK:客户端收到SYN-ACK包后,发送一个ACK(确认)包给服务器,确认连接建立。

    Client -> Server: ACK, Seq = x + 1, Ack = y + 1

完成这三次握手后,TCP连接建立成功,双方可以开始数据传输。

四次挥手(Four-way Handshake)

四次挥手是终止TCP连接的过程,确保双方都能正常关闭连接。具体步骤如下:

  1. FIN:主动关闭连接的一方(通常是客户端)发送一个FIN(结束)包,表示它已经完成数据发送。

    Client -> Server: FIN, Seq = x
  2. ACK:服务器收到FIN包后,发送一个ACK包,确认收到FIN包。

    Server -> Client: ACK, Seq = y, Ack = x + 1
  3. FIN:服务器准备关闭连接时,发送一个FIN包给客户端,表示服务器也完成了数据发送。

    Server -> Client: FIN, Seq = y, Ack = x + 1
  4. 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 integerstring 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. 自定义协议解析

对于自定义的数据格式或协议,需要根据协议规范手动解析数据。

  • 解析步骤
    1. 确定数据格式和结构。
    2. 读取数据流。
    3. 根据结构解析数据字段。
    4. 处理解析后的数据。

16.序列化和序列化

序列化(Serialization)

序列化是将对象的状态信息转换为可以存储或传输的格式(如XML、JSON、二进制等)的过程。序列化后的数据显示为字节流,可以保存到文件、数据库或通过网络传输。

序列化的目的:

  1. 持久化:将对象状态保存到文件或数据库中。
  2. 网络传输:在网络上发送对象状态。
  3. 跨平台数据交换:在不同的操作系统或平台之间交换数据。

序列化的类型:

  1. 文本序列化:如JSON和XML,易于阅读和调试,但可能体积较大。
  2. 二进制序列化:如二进制格式,体积较小,读写速度快,但不如文本格式易于阅读。

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、二进制等)转换回对象状态的过程。反序列化允许从存储介质或网络接收的数据重新构建对象。

反序列化的目的:

  1. 数据恢复:从文件或数据库中恢复对象状态。
  2. 数据接收:接收网络上传输的对象状态。
  3. 跨平台数据使用:在不同的操作系统或平台中使用交换的数据。

C#中反序列化的示例(JSON):

using Newtonsoft.Json;
string json = "{\"Name\":\"John\",\"Age\":30}";
Person person = JsonConvert.DeserializeObject<Person>(json);

注意事项

  1. 性能:序列化和反序列化可能会影响性能,尤其是在处理大量数据时。
  2. 安全性:序列化和反序列化时要注意安全问题,避免序列化敏感数据或反序列化不可信的数据。
  3. 版本兼容性:在软件版本更新时,需要考虑序列化格式的兼容性问题。
  4. 数据完整性:确保序列化和反序列化过程中数据的完整性和一致性。

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. 使用asyncawait

C# 5.0引入了asyncawait关键字,使得异步编程更加简洁和易于管理。

// 使用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和线程池的区别

  1. 抽象层次

    • Task是任务并行库(TPL)的一部分,它在线程池之上提供了一个更高层次的抽象。Task对象封装了异步操作,使得开发者可以更容易地管理异步代码和处理操作结果。
    • 线程池是.NET提供的低层次服务,用于管理和重用线程,减少创建和销毁线程的开销。
  2. 使用方式

    • Task提供了更简洁的API来创建和管理异步操作。例如,Task.Run是一个快捷方式,用于在线程池线程上执行代码。
    • 线程池通常通过ThreadPool.QueueUserWorkItem方法来排队工作项,这些工作项将在线程池的线程上异步执行。
  3. 任务调度

    • Task可以在同一个线程中顺序执行多个任务,减少上下文切换,而线程池则可能在不同的线程之间调度任务。
    • Task提供了更好的任务调度和错误处理机制,以及对任务完成的响应(如ContinueWith)。
  4. 返回值和状态

    • Task可以很容易地处理返回值和任务状态,例如通过Task.Result属性获取任务结果。
    • 线程池的工作项通常不直接返回值,需要通过其他机制(如回调或共享状态)来处理结果。
  5. 线程创建和管理

    • Task创建的是线程池任务,而Thread默认创建的是前台线程。Task在内部使用了线程池,但提供了更细粒度的控制。
    • 线程池的工作项通常只运行执行时间较短的异步操作,而Task可以用于长时间运行的任务,尽管这可能涉及到创建新的线程。
  6. 性能优化

    • Task对线程池进行了优化,例如在同一个线程中顺序执行多个任务,减少任务上下文切换带来的时间浪费。
  7. 取消和异常处理

    • 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) =>
{// 安全地更新UImyTextBox.Text = "操作完成";
};// 启动后台工作线程
backgroundWorker1.RunWorkerAsync();

4. Control.InvokeRequired 属性

Control.InvokeRequired 属性指示是否需要使用 Invoke 来跨线程访问控件。

if (myControl.InvokeRequired)
{myControl.Invoke((MethodInvoker)delegate{// 安全地更新UImyTextBox.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架构的一些关键特点和组件:

特点

  1. 跨平台性:用户可以通过任何支持Web浏览器的设备访问应用程序,不受操作系统限制。
  2. 集中式管理:应用程序的更新和维护都在服务器端进行,简化了管理流程。
  3. 可扩展性:服务器可以根据需求进行扩展,以支持更多的用户和更高的负载。
  4. 成本效益:用户不需要安装和维护客户端软件,降低了总体拥有成本。
  5. 安全性:安全策略可以在服务器端集中实施,如使用SSL/TLS加密通信。

组件

  1. 客户端(Browser)

    • 用户通过Web浏览器访问应用程序。
    • 浏览器作为客户端,负责显示内容、收集用户输入和发送请求到服务器。
  2. 服务器(Server)

    • 处理业务逻辑、数据存储和检索。
    • 可以进一步细分为应用服务器和数据库服务器。
    • 应用服务器处理应用程序逻辑,数据库服务器管理数据存储。
  3. 网络

    • 互联网作为客户端和服务器之间的通信媒介。
    • 使用HTTP/HTTPS等协议进行数据传输。
  4. 数据库

    • 存储应用程序的数据。
    • 可以是关系型数据库(如MySQL、PostgreSQL)或非关系型数据库(如MongoDB)。

工作流程

  1. 用户通过浏览器发送请求到服务器。
  2. 服务器接收请求,处理业务逻辑。
  3. 服务器从数据库检索或存储数据。
  4. 服务器将处理结果(通常是HTML页面、JSON数据或其他资源)返回给浏览器。
  5. 浏览器渲染结果,展示给用户。

技术栈

  • 前端技术:HTML、CSS、JavaScript、框架(如React、Angular、Vue.js)。
  • 后端技术:服务器端编程语言(如Java、.NET、PHP、Python)、Web框架(如Spring、Django、Express.js)。
  • 数据库技术:MySQL、PostgreSQL、MongoDB、SQL Server等。

23.缓存数据库的常用的数据类型

缓存数据库是用来存储频繁访问的数据以提高性能的系统。在缓存数据库中,常用的数据类型包括:

  1. 字符串(String)

    • 用于存储文本数据,如用户信息、配置设置等。
  2. 整数(Integer)

    • 用于存储整数值,如计数器、状态码等。
  3. 浮点数(Float/Double)

    • 用于存储小数点数值,如价格、计算结果等。
  4. 布尔值(Boolean)

    • 用于存储真/假值,如标志位、开关状态等。
  5. 列表(List)

    • 用于存储有序集合,如用户列表、权限列表等。
  6. 集合(Set)

    • 用于存储无序且不重复的元素集合,如唯一标识符集合等。
  7. 有序集合(Sorted Set)

    • 用于存储带有分数(或权重)的元素集合,元素可以按分数排序,如排行榜等。
  8. 哈希表(Hash)

    • 用于存储键值对集合,如对象属性的缓存等。
  9. 二进制数据(Binary Data)

    • 用于存储图片、文件等二进制数据。
  10. 时间戳(Timestamp)

    • 用于记录时间信息,如最后访问时间、过期时间等。
  11. 地理空间数据类型(Geospatial Data Types)

    • 用于存储地理位置信息,如经纬度等,支持地理空间查询。
  12. 超日志(HyperLogLog)

    • 用于基数统计,估计一个集合中不同元素的数量,占用空间小,适用于大数据量。
  13. 位图(Bitmap)

    • 用于位操作,可以用于实现布隆过滤器等。
  14. JSON/其他序列化对象

    • 用于存储复杂结构的数据,如JSON对象等。
  15. 游标(Cursor)

    • 用于在迭代大量数据时保持状态,如分页查询等。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com