C# .NET面试系列三:集合、异常、泛型、LINQ、委托、EF!
集合、异常、泛型、LINQ、委托、EF!
1. IList 接口与 List 的区别是什么?
IList 接口和 List 类是C#中集合的两个相关但不同的概念。下面是它们的主要区别:
IList 接口
IList 接口是C#中定义的一个泛型接口,位于 System.Collections 命名空间。它派生自 ICollection 接口,定义了一个可以通过索引访问的有序集合。
IList 接口包含一系列索引化的属性和方法,允许按索引访问、插入、移除元素等。
由于是接口,它只定义了成员的契约,而不提供具体的实现。类似于 IEnumerable 接口,实现 IList 接口的类需要提供具体的实现。
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this[int index] { get; set; }
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
List 类
List 类是C#中的一个具体实现,位于 System.Collections.Generic 命名空间。它实现了 IList<T> 接口,提供了一个动态大小的数组,支持快速的索引访问、插入、删除等操作。
List 类实现了 IList<T> 接口中定义的所有成员,同时还提供了其他一些用于集合操作的方法。
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable
{
// 具体的实现
}
总结区别:
IList 是一个接口,定义了有序集合的契约,需要由具体类提供实现。
List 是一个具体的实现,实现了 IList<T> 接口,提供了对动态数组的操作。
在大多数情况下,你会更倾向于使用 List<T> 类,因为它提供了一些方便的方法和性能优化,而且可以直接使用。IList 接口在某些特定情境下可能会被用作更泛化的引用,以便接受多种实现。
2. 泛型的主要约束和次要约束是什么?
在C#中,泛型约束(constraints)用于限制泛型类型参数的类型。主要约束和次要约束是泛型约束中的两种类型。
1、主要约束(Primary Constraints)
主要约束是指通过 where 子句指定的对泛型类型参数的基本要求。主要约束包括以下几种:
class 约束:
指定泛型类型参数必须是引用类型(类、接口、委托或数组)。
public class Example<T> where T : class
{
// T 必须是引用类型
}
struct 约束:
指定泛型类型参数必须是值类型(结构)。
public class Example<T> where T : struct
{
// T 必须是值类型
}
new() 约束:
指定泛型类型参数必须具有无参数的公共构造函数。
public class Example<T> where T : new()
{
// T 必须有无参数的构造函数
}
2、次要约束(Secondary Constraints)
次要约束是指通过 where 子句指定的对泛型类型参数的额外约束。次要约束包括以下几种:
派生类约束:
指定泛型类型参数必须是指定的基类或实现的接口。
public class Example<T> where T : MyBaseClass
{
// T 必须是 MyBaseClass 的派生类
}
接口约束:
指定泛型类型参数必须实现指定的接口。
public class Example<T> where T : IMyInterface
{
// T 必须实现 IMyInterface 接口
}
构造函数约束:
指定泛型类型参数必须具有指定的构造函数。
public class Example<T> where T : MyBaseClass, new()
{
// T 必须是 MyBaseClass 的派生类,且有无参数的构造函数
}
通过使用主要约束和次要约束,可以对泛型类型参数进行更精确的限制,以满足特定的需求。这样可以在编译时提供更多的类型安全性。
3. 如何把一个 array 复制到 arrayist 里?
要将一个数组(Array)复制到一个 ArrayList 中,你可以使用 ArrayList 类的 AddRange 方法。以下是一个示例:
using System;
using System.Collections;
class Program
{
static void Main()
{
// 原始数组
int[] array = { 1, 2, 3, 4, 5 };
// 创建 ArrayList
ArrayList arrayList = new ArrayList();
// 使用 AddRange 方法将数组复制到 ArrayList
arrayList.AddRange(array);
// 打印 ArrayList 中的元素
foreach (var item in arrayList)
{
Console.WriteLine(item);
}
}
}
在上述示例中,AddRange 方法接受一个 ICollection 类型的参数,而数组是 ICollection,因此可以直接传递数组作为参数。这样,数组的元素就会被逐一添加到 ArrayList 中。
请注意,如果你使用的是.NET Framework 2.0或更高版本,建议使用泛型集合 List<T> 替代非泛型的 ArrayList。泛型集合提供了更好的类型安全性和性能。以下是使用 List<T> 的示例:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 原始数组
int[] array = { 1, 2, 3, 4, 5 };
// 创建 List<int>
List<int> list = new List<int>();
// 使用 AddRange 方法将数组复制到 List<int>
list.AddRange(array);
// 打印 List<int> 中的元素
foreach (var item in list)
{
Console.WriteLine(item);
}
}
}
这样可以得到更好的类型检查和性能。
4. List, Set, Map 是否继承自 Collection 接口?
在C#中,List, HashSet, 和 Dictionary(用于实现 Map)都实现了 ICollection<T> 接口和 IEnumerable<T> 接口,但它们并没有直接继承自 Collection<T> 类。
List<T>
List<T> 实现了 ICollection<T> 和 IEnumerable<T> 接口,它是一个动态数组,提供对元素的快速访问和操作。
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IReadOnlyList, IReadOnlyCollection
{
// 具体实现
}
HashSet<T>
HashSet<T> 实现了 ICollection<T> 和 IEnumerable<T> 接口,它表示一个不包含重复元素的集合。
public class HashSet<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>
{
// 具体实现
}
Dictionary<TKey, TValue>
Dictionary<TKey, TValue> 实现了 ICollection<KeyValuePair<TKey, TValue>> 和 IEnumerable<KeyValuePair<TKey, TValue>> 接口,它是键值对的集合。
public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable, IReadOnlyDictionary<TKey, TValue>, IReadOnlyCollection<KeyValuePair<TKey, TValue>>
{
// 具体实现
}
虽然这些类没有直接继承自 Collection<T>,但它们提供了一组方法和属性,使得它们可以被当作集合来使用。这些类都属于 System.Collections.Generic 命名空间。如果你需要使用更具体的集合功能,可以考虑使用它们提供的接口或使用更专门的集合类型。
5. Set 里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用 == 还是equals()? 它们有何区别?
在C#中,HashSet<T> 是用来表示一个集合,其中的元素是唯一的,不允许重复。元素的唯一性是通过 Equals 方法来判断的,而不是 == 运算符。区别如下:
Equals 方法
Equals 方法是用于比较两个对象是否相等的方法。在 HashSet<T> 中,它用于判断两个元素是否相等,从而保持集合中的元素唯一性。
如果你的元素类型没有重写 Equals 方法,默认行为是比较对象的引用(即内存地址),而不是对象的内容。
public class Person
{
public string Name { get; set; }
}
Person person1 = new Person { Name = "John" };
Person person2 = new Person { Name = "John" };
HashSet<Person> personSet = new HashSet<Person>();
personSet.Add(person1);
personSet.Add(person2); // 这里会成功添加,因为默认的 Equals 比较的是引用
Console.WriteLine(personSet.Count); // 输出 2
== 运算符
== 运算符在C#中是用于比较值类型和引用类型的。对于引用类型,== 比较的是对象的引用,而不是内容。
通常情况下,需要重写 == 运算符才能使其按照你的期望进行比较。但是,在 HashSet<T> 中,仍然会使用元素的 Equals 方法来确定唯一性,而不是 == 运算符。
public class Person
{
public string Name { get; set; }
public static bool operator ==(Person person1, Person person2)
{
if (ReferenceEquals(person1, person2))
return true;
if (person1 is null || person2 is null)
return false;
return person1.Name == person2.Name;
}
public static bool operator !=(Person person1, Person person2)
{
return !(person1 == person2);
}
}
Person person1 = new Person { Name = "John" };
Person person2 = new Person { Name = "John" };
HashSet<Person> personSet = new HashSet<Person>();
personSet.Add(person1);
personSet.Add(person2); // 这里只会添加一个元素,因为重写了 == 运算符
Console.WriteLine(personSet.Count); // 输出 1
总的来说,HashSet<T> 使用元素的 Equals 方法来判断唯一性,因此你需要确保你的元素类型正确实现了 Equals 方法。如果需要使用 == 运算符,你需要自己重写它以确保按照你的期望进行比较。
6. 有 50 万个 int 类型的数字,现在需要判断一下里面是否存在重复的数字,请你简要说一下思路。
对于判断是否存在重复的数字,你可以考虑以下几种常见的方法:
1、使用 HashSet
将所有的 int 数字添加到 HashSet 中,HashSet 会自动去重。如果最终 HashSet 的大小与原始数组的大小相等,说明没有重复元素。
int[] numbers = /* your array of 500,000 int numbers */;
HashSet<int> uniqueNumbers = new HashSet<int>(numbers);
bool hasDuplicates = uniqueNumbers.Count != numbers.Length;
2、排序后检查相邻元素
将数组进行排序,然后检查相邻的元素是否相等。如果相邻元素相等,说明存在重复数字。
int[] numbers = /* your array of 500,000 int numbers */;
Array.Sort(numbers);
bool hasDuplicates = false;
for (int i = 1; i < numbers.Length; i++)
{
if (numbers[i] == numbers[i - 1])
{
hasDuplicates = true;
break;
}
}
3、使用 LINQ
使用 LINQ 查询是否存在重复元素。
int[] numbers = /* your array of 500,000 int numbers */;
bool hasDuplicates = numbers.GroupBy(x => x).Any(g => g.Count() > 1);
选择适用于你特定情况的方法,考虑到性能和实现的简洁性。HashSet 的方法是最直观和高效的,但也要考虑到排序的方法,特别是在原始数组已经有序的情况下。
参考:
1.使用C#的List集合自带的去重方法,例如 Distinct(),GroupBy()等
2.利用 Dictionary 的Key值唯一的特性,HashSet 元素值唯一的特性 进行判断
7. 数组有没有 length() 这个方法? String 有没有 length() 这个方法?
在C#中,数组和字符串都没有名为 length() 的方法。而是使用属性来获取它们的长度信息。
数组(Array)
数组使用 Length 属性来获取它们的长度,不是方法。
int[] numbers = { 1, 2, 3, 4, 5 };
int arrayLength = numbers.Length;
字符串(String)
字符串使用 Length 属性来获取它们的长度,同样也不是方法。
string text = "Hello, World!";
int stringLength = text.Length;
总结:在C#中,数组和字符串的长度信息都通过属性 Length 来获取,而不是通过名为 length() 的方法。
8. 一个整数 List 中取出最大数(找最大值)。不能用 Max 方法。
private static int GetMax(List<int> list)
{
if (list == null || list.Count == 0)
{
throw new ArgumentException("List is null or empty");
}
int max = list[0];
for (int i = 0; i < list.Count; i++)
{
if (list[i]>max)
{
max = list[i];
}
}
return max;
}
9. C# 异常类有哪些信息?
C#中,所有异常都继承自System.Exception类,Exception类定义了C#异常应该具有的信息和方法。值得注意的属性有:
public virtual string? StackTrace { get; } // 获取异常的调用堆栈。如果未提供调用堆栈信息,则为 null
public virtual string? Source { get; set; } // 获取或设置引发错误的应用程序或对象的名称。可能引发 ArgumentException 异常,要求对象必须是运行时的 System.Reflection 对象
public virtual string Message { get; } // 获取描述当前异常的消息。如果没有提供错误原因的消息,则返回空字符串
public Exception? InnerException { get; } // 获取引起当前异常的 System.Exception 实例。如果未在构造函数中提供内部异常值,则返回 null
public int HResult { get; set; } // 获取或设置 HRESULT,这是分配给特定异常的编码数值
public virtual IDictionary Data { get; } // 获取包含关于异常的额外用户定义信息的键/值对集合。默认为空集合
public MethodBase? TargetSite { get; } // 获取引发当前异常的 System.Reflection.MethodBase
public virtual string? HelpLink { get; set; } // 获取或设置与此异常关联的帮助文件的链接,以 URN 或 URL 的形式表示
10. 如何创建一个自定义异常?
在C#中,你可以创建一个自定义异常类,通常是通过继承 System.Exception 或其派生类来实现的。以下是创建自定义异常的基本步骤:
1、创建自定义异常类
创建一个类,并继承 System.Exception 或其派生类。你可以添加自己的构造函数、属性或其他方法。
using System;
public class CustomException : Exception
{
public CustomException()
{
}
public CustomException(string message) : base(message)
{
}
public CustomException(string message, Exception innerException) : base(message, innerException)
{
}
// 可以添加其他属性或方法
}
在上面的例子中,CustomException 继承自 Exception 类,并提供了一些构造函数,允许你在创建异常对象时传递不同的信息。
2、在代码中引发自定义异常
在代码中,使用 throw 语句引发自定义异常。
public class Example
{
public void SomeMethod()
{
// 某些条件下引发自定义异常
throw new CustomException("This is a custom exception.");
}
}
在这个例子中,当 SomeMethod 被调用并满足某些条件时,会抛出 CustomException。
3、捕获和处理自定义异常
在调用可能引发自定义异常的代码块中使用 try-catch 块来捕获和处理异常。
try
{
Example example = new Example();
example.SomeMethod();
}
catch (CustomException ex)
{
Console.WriteLine("Caught custom exception: " + ex.Message);
}
这样,你就可以在自己的应用程序中定义和使用自定义异常类。确保在定义自定义异常类时,提供适当的构造函数,以便在引发异常时传递相关的信息。
11. 利用 IEnumerable 实现斐波那契数列生成?
你可以使用 IEnumerable<T> 接口来实现生成斐波那契数列的迭代器。以下是一个简单的例子:
using System.Collections;
public class FibonacciGenerator : IEnumerable<int>
{
private int count;
public FibonacciGenerator(int count)
{
if (count < 0)
throw new ArgumentException("Count must be non-negative.");
this.count = count;
}
public IEnumerator<int> GetEnumerator()
{
int a = 0, b = 1;
for (int i = 0; i < count; i++)
{
yield return a;
int temp = a;
a = b;
b = temp + b;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class Program
{
static void Main()
{
FibonacciGenerator fibonacci = new FibonacciGenerator(10);
foreach (int number in fibonacci)
{
Console.WriteLine(number);
}
}
}
在上面的例子中,FibonacciGenerator 类实现了 IEnumerable<int> 接口,它包含一个私有字段 count 用于指定生成的斐波那契数列的长度。GetEnumerator 方法使用 yield return 语句实现迭代器,生成斐波那契数列的元素。最后,在 Main 方法中,我们创建了 FibonacciGenerator 的实例,并使用 foreach 循环遍历生成的数列。
12. 请利用 foreach 和 ref 为一个数组中的每个元素加 1
注意 foreach 不能用 var ,也不能直接用 int ,需要 ref int ,注意 arr 要转换为 Span 。
int[] arr = { 1, 2, 3, 4, 5};
Console.WriteLine(string.Join(",", arr)); // 1,2,3,4,5
foreach (ref int v in arr.AsSpan())
{
v++;
}
Console.WriteLine(string.Join(",", arr)); // 2,3,4,5,6
13. 如何针对不同的异常进行捕捉?
try
{
// 一些可能引发异常的代码
string value = null;
int length = value.Length; // 这里会引发 NullReferenceException
}
catch (NullReferenceException ex)
{
// 处理空指针异常
Console.WriteLine("发生空指针异常:" + ex.Message);
}
catch (ArgumentException ex)
{
// 处理参数异常
Console.WriteLine("发生参数异常:" + ex.Message);
}
catch (Exception ex)
{
// 处理其他类型的异常
Console.WriteLine("发生其他异常:" + ex.Message);
}
finally
{
// 可选的 finally 块,用于执行无论是否发生异常都必须执行的代码
Console.WriteLine("无论是否发生异常,都会执行的代码");
}
14. 如何避免类型转换时的异常?
避免类型转换时的异常通常涉及使用安全的转换方法以及在必要时进行类型检查。以下是一些建议:
使用安全类型转换方法
在进行类型转换时,使用 as 操作符或 TryParse 方法(对于数值类型)等安全的转换方法。这些方法在无法转换时不会引发异常,而是返回 null 或 false。
// 使用 as 操作符
string str = "123";
int? result = str as int?;
// 或者使用 TryParse
string str = "123";
if (int.TryParse(str, out int result))
{
// 转换成功
}
else
{
// 转换失败
}
使用模式匹配
在C# 7及更高版本中,可以使用模式匹配来进行类型检查和转换。
object obj = "123";
if (obj is int intValue)
{
// 转换成功,可以使用 intValue
}
else
{
// 转换失败
}
使用 as 操作符和空值合并运算符
如果你确实需要使用强制转换,可以使用 as 操作符和空值合并运算符 ?? 来提供默认值。
object obj = "123";
int result = (obj as int?) ?? 0;
使用泛型方法
如果你需要在多个地方进行类型转换,可以考虑使用泛型方法来避免重复的代码,并提高代码的重用性。
public T ConvertTo<T>(object value, T defaultValue = default(T))
{
return value is T result ? result : defaultValue;
}
// 使用
object obj = "123";
int result = ConvertTo<int>(obj, 0);
避免直接使用强制类型转换
尽量避免直接使用强制类型转换,特别是在不确定类型的情况下。直接使用强制类型转换可能引发 InvalidCastException。
// 避免
object obj = "123";
int result = (int)obj; // 可能引发 InvalidCastException
通过采取这些措施,你可以更安全地进行类型转换,并减少因为类型不匹配而引发的异常。
15. Serializable 特性有什么作用?
Serializable 特性是.NET框架中的一个特性,用于标记类,表示该类的实例可以在网络上或者存储设备中进行序列化和反序列化。序列化是将对象转换为字节流的过程,而反序列化是将字节流转换回对象的过程。
主要作用包括
1、对象持久化
允许将对象的状态保存到磁盘或数据库中,以便在程序重新启动后能够恢复对象的状态。
2、远程通信
通过网络传输序列化的对象,实现在分布式系统中的对象传递。
3、跨应用程序域通信
在.NET中,应用程序域(AppDomain)是一个应用程序的隔离单元。通过序列化,可以在不同应用程序域之间传递对象。
4、跨平台通信
允许在不同平台(如Windows和Linux)之间序列化和反序列化对象。
为了使用 Serializable 特性,你只需在类声明上添加 [Serializable] 特性标记:
[Serializable]
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
}
需要注意的是,当你使用 Serializable 特性时,类的所有成员变量(字段、属性等)都应该是可序列化的,否则可能引发 SerializationException。如果你有一些成员不希望被序列化,可以使用 [NonSerialized] 特性标记这些成员。
16. 委托是什么?
在C#中,委托(Delegate)是一种类型,它代表对一个或多个方法的引用。委托可以用来定义方法的签名,以及在运行时将方法绑定到委托实例。委托提供了一种间接调用方法的机制,使得可以在运行时动态地切换和调用不同的方法。
主要特点和作用
1、类型安全的函数指针
委托是类型安全的,它们包含方法的签名,确保在编译时检查方法的兼容性。
2、多播委托
一个委托实例可以同时引用多个方法,这称为多播委托。当调用多播委托时,它会按照委托列表中的顺序调用所有方法。
3、异步编程
委托常用于异步编程,特别是在事件驱动的编程模型中。事件就是委托的一种应用。
4、回调函数
可以将委托用作回调函数,将方法作为参数传递给其他方法,以实现回调机制。
5、解耦
委托可以用于解耦代码,使得代码更灵活、可维护,并支持面向对象设计的原则。
以下是一个简单的委托示例:
// 定义一个委托类型
public delegate void MyDelegate(string message);
public class Program
{
// 委托引用的方法
public static void DisplayMessage(string message)
{
Console.WriteLine("Message: " + message);
}
public static void Main()
{
// 创建委托实例并绑定方法
MyDelegate myDelegate = new MyDelegate(DisplayMessage);
// 调用委托
myDelegate("Hello, World!");
}
}
在上述代码中,MyDelegate 委托定义了一个方法签名,它接受一个 string 参数。然后,通过创建委托实例 myDelegate 并将 DisplayMessage 方法绑定到委托上,最终调用委托实例来调用 DisplayMessage 方法。这就是委托的基本用法。
17. 如何自定义委托?
在C#中,你可以通过声明一个委托类型来自定义委托。委托类型定义了委托可以引用的方法的签名。以下是自定义委托的基本步骤:
1、声明委托类型
// 使用 delegate 关键字声明一个委托类型,指定委托的参数类型和返回类型。
public delegate void MyDelegate(string message);
// 在上述代码中,MyDelegate 是一个委托类型,它接受一个 string 类型的参数,且没有返回值。
2、创建委托实例
// 使用委托类型创建一个委托实例。可以通过实例化委托类型并传递一个方法(或一组方法)来初始化委托实例。
MyDelegate myDelegate = new MyDelegate(DisplayMessage);
// 在上述代码中,DisplayMessage 是一个符合 MyDelegate 委托签名的方法,它可以被委托引用。
3、绑定方法
// 将一个或多个方法绑定到委托实例。可以通过使用 += 运算符来添加方法,形成多播委托。
myDelegate += AnotherMethod;
// 在上述代码中,AnotherMethod 是另一个符合 MyDelegate 委托签名的方法。
4、调用委托
// 使用委托实例调用委托引用的方法。委托的调用方式与调用方法类似。
myDelegate("Hello, World!");
完整的示例代码如下:
public delegate void MyDelegate(string message);
public class Program
{
public static void DisplayMessage(string message)
{
Console.WriteLine("DisplayMessage: " + message);
}
public static void AnotherMethod(string message)
{
Console.WriteLine("AnotherMethod: " + message);
}
public static void Main()
{
// 创建委托实例并绑定方法
MyDelegate myDelegate = new MyDelegate(DisplayMessage);
myDelegate += AnotherMethod;
// 调用委托
myDelegate("Hello, World!");
}
}
在上述示例中,MyDelegate 委托引用了两个方法:DisplayMessage 和 AnotherMethod。调用委托时,会依次调用绑定的方法。
18. .NET 默认的委托类型有哪几种?
.NET框架提供了一些默认的委托类型,其中最常用的包括:
Action 委托:
表示一个不返回值的方法,可以接受零到六个参数。
Action action1 = () => Console.WriteLine("Action without parameters");
Action<string> action2 = (s) => Console.WriteLine("Action with parameter: " + s);
Func 委托:
表示一个有返回值的方法,可以接受零到六个输入参数。
Func<int> func1 = () => 42;
Func<string, int> func2 = (s) => s.Length;
Predicate 委托:
表示一个接受一个参数并返回布尔值的方法,通常用于判断条件。
Predicate<int> predicate = (i) => i > 0;
这些委托类型提供了一种更简洁的方式来定义和使用委托,特别是在使用Lambda表达式时。Action 和 Func 委托是比较通用的,而 Predicate 委托专门用于表示布尔条件。
另外,还有一些特定用途的委托类型,如:
EventHandler 委托:
用于处理事件的委托,通常用于订阅和处理事件。
Comparison 委托:
用于比较两个对象的顺序。
Converter 委托:
用于将一个类型转换为另一个类型。
这些委托类型在不同的场景中提供了更专门的功能,使得在编写代码时更加方便和灵活
19. 什么是泛型委托?
泛型委托(Generic Delegate)是使用泛型参数的委托类型。泛型委托可以用于引用任意类型的方法,而不需要在委托声明时指定具体的方法签名。这使得泛型委托更加灵活,可以适应不同的方法签名。
在C#中,Func 和 Action 委托是泛型委托的常见例子。
Func 委托:
Func 委托是一个泛型委托,可以引用具有指定返回类型和参数类型的方法。最后一个泛型参数表示返回类型。
Func<int, string, bool> funcDelegate = (i, s) => i.ToString() == s;
上述代码中,Func 委托可以引用一个接受一个整数和一个字符串参数,并返回布尔值的方法。
Action 委托:
Action 委托表示一个不返回值的方法,也是一个泛型委托。它可以引用具有不同参数类型的方法。
Action<int, string> actionDelegate = (i, s) => Console.WriteLine($"{i}: {s}");
上述代码中,Action 委托可以引用一个接受一个整数和一个字符串参数的方法,但不返回值。
泛型委托的优势在于可以在声明委托时动态指定方法签名,而不需要为每个不同的方法签名创建一个新的委托类型。这使得委托更加通用和适用于各种情况。
20. 什么是匿名方法?
匿名方法是在C#中引入的一种方式,允许在不定义具体命名方法的情况下直接声明和使用方法。匿名方法通常用于传递给委托,尤其是在事件处理、多线程编程或 LINQ 查询等场景中。
匿名方法的基本语法如下:
delegateType delegateName = delegate(parameters)
{
// 方法体
};
其中,delegateType 是委托的类型,delegateName 是委托的实例名,parameters 是方法的参数列表,而后面的块包含了匿名方法的实际实现。
以下是一个简单的匿名方法的例子:
public delegate void MyDelegate(string message);
public class Program
{
public static void Main()
{
// 使用匿名方法创建委托实例
MyDelegate myDelegate = delegate(string message)
{
Console.WriteLine("Anonymous Method: " + message);
};
// 调用委托
myDelegate("Hello, World!");
}
}
在上述代码中,匿名方法被用来实现委托 MyDelegate,并在调用委托时输出消息。
匿名方法的优点是它可以直接嵌套在委托的声明中,而无需创建具体的方法。然而,随着C#的版本升级,Lambda 表达式成为了更为简洁和强大的替代方案,因此在新的代码中,Lambda 表达式通常更受欢迎。Lambda 表达式是匿名方法的一种更现代的语法糖。
21. 什么是闭包?
闭包(Closure)是一种特殊的函数对象,它包含了函数定义时创建的词法作用域中的变量。换句话说,闭包允许一个函数在其声明的词法作用域之外引用变量。
在理解闭包之前,首先需要了解一下词法作用域(Lexical Scope)和函数的作用域。
词法作用域:
词法作用域是指在代码编写阶段确定变量作用域的规则。它是由代码的结构和嵌套关系来定义的,而不是在运行时动态确定的。在词法作用域中,函数可以访问其声明时所处的作用域中的变量。
闭包:
当一个函数引用了其声明时的作用域外的变量时,就形成了闭包。闭包允许函数在其声明的作用域之外记住和访问这些变量。
以下是一个简单的闭包的例子:
public class ClosureExample
{
public static Action<int> CreateClosure()
{
int outerVariable = 10;
// 返回一个闭包
return (x) =>
{
int result = x + outerVariable;
Console.WriteLine("Result: " + result);
};
}
public static void Main()
{
// 创建一个闭包
Action<int> closure = CreateClosure();
// 调用闭包
closure(5); // 输出:Result: 15
}
}
在上述代码中,CreateClosure 方法返回一个闭包,这个闭包引用了其外部作用域中的 outerVariable。当调用闭包时,它仍然能够访问和使用 outerVariable 的值,即使 CreateClosure 方法已经执行完毕。
闭包的存在使得在函数内部定义的函数可以保留对其外部作用域中变量的引用,从而形成更灵活的代码结构。
22. EF(Entity Framework) 是什么?
Entity Framework(EF)是Microsoft提供的一种对象关系映射(ORM)框架,用于在.NET应用程序中简化对数据库的访问和操作。它允许开发人员使用.NET对象来表示数据库中的数据,并提供了一种将这些对象映射到数据库表的方式,使得数据库操作更加面向对象和便捷。
主要特点和功能包括:
1、对象关系映射(ORM)
EF允许开发人员使用.NET实体类表示数据库中的表和数据,无需直接编写SQL语句。EF负责将实体类的属性映射到数据库表的字段,并处理对象与数据库之间的转换。
2、LINQ支持
EF提供对LINQ(Language Integrated Query)的全面支持,使得在.NET应用程序中可以使用LINQ查询来操作数据库,提高了查询的表达能力和可读性。
3、自动迁移
EF支持自动迁移(Automatic Migrations),可以根据实体类的更改自动更新数据库模式,简化了数据库迭代和版本管理的过程。
4、数据访问
EF提供了一组API,使得开发人员可以轻松进行常见的数据库操作,如插入、更新、删除和查询数据。开发人员可以使用LINQ查询语言或者直接使用查询构建器进行数据库查询。
5、支持多种数据库
EF支持多种关系型数据库,包括但不限于SQL Server、MySQL、Oracle、SQLite等,使得开发人员可以在不同的数据库系统中使用相同的代码。
6、性能优化
EF提供了一些性能优化功能,如延迟加载(Lazy Loading)、预先加载(Eager Loading)等,以便在需要时从数据库中加载相关数据,提高了查询的效率。
7、代码优先
EF支持代码优先(Code First)开发方式,开发人员可以通过编写实体类来定义数据库模型,然后通过迁移生成数据库,而不是通过数据库先有表结构再生成实体类。
Entity Framework的目标是简化数据库访问,并提供一种更抽象和面向对象的方式来处理数据库操作。
23. 什么是 ORM ?
ORM (Object-Relational Mapping) 是一种编程技术,它允许将面向对象的编程语言(如C#)中的对象模型与关系数据库之间进行映射。ORM 的目标是在应用程序中使用面向对象的方式操作数据库,而无需直接处理底层的关系数据库细节。
通过使用 ORM,开发人员可以使用面向对象的代码来表示数据库中的表和记录,而无需手动编写和执行 SQL 查询。ORM 框架负责将对象模型与数据库结构进行映射,以及在应用程序中执行数据库操作。这样,开发人员可以更专注于业务逻辑而不必过于关心数据库的具体实现细节。
一些常见的 C# ORM 框架包括:
- Entity Framework (EF): 是由 Microsoft 开发的官方 ORM 框架,支持多种数据库系统,并提供强大的 LINQ 查询功能。
- NHibernate: 是一个开源的 ORM 框架,基于 Java 的 Hibernate 框架,提供了对关系数据库的映射和查询功能。
- Dapper: 虽然它更接近于微型 ORM,但它是一个轻量级的、快速的对象关系映射框架,由 Stack Overflow 团队开发。
ORM 的使用可以简化数据库操作,提高开发效率,并减少与数据库相关的错误。然而,开发人员应该了解 ORM 的工作原理,以便更好地优化和理解底层数据库访问的性能和行为。
24. 为什么用 EF 而不用原生的 ADO.NET ?
使用Entity Framework(EF)而不是原生的ADO.NET有一些优势,尤其是在开发过程中和面向对象的编程模型方面。以下是一些选择EF而不使用原生ADO.NET的理由:
1、面向对象的编程模型
EF提供了面向对象的编程模型,允许使用.NET实体类来表示数据库中的表和数据。这种对象关系映射(ORM)的方式更符合面向对象编程的理念,使得开发人员可以使用类和对象的概念而不是直接的数据表和SQL语句。
2、LINQ查询
EF支持LINQ(Language Integrated Query),这使得在.NET应用程序中可以使用强大的查询语言来操作数据库。LINQ提供了一种更直观和可读性更高的方式来编写查询,而不需要编写复杂的SQL语句。
3、自动迁移
EF支持自动迁移,能够根据实体类的更改自动更新数据库模式。这简化了数据库的版本管理和升级过程,使得开发人员不必手动维护数据库脚本。
4、少量的代码
使用EF通常需要编写较少的代码,因为它处理了大部分的数据库访问细节。相比之下,原生ADO.NET需要编写更多的代码来打开连接、执行命令、处理DataReader等。
5、开发效率
EF提供了高层次的抽象,简化了开发过程,提高了开发效率。开发人员可以更专注于业务逻辑的实现,而不必过多关注数据库访问的底层细节。
6、适应不同数据库
EF支持多种关系型数据库,可以轻松切换数据库后端而无需更改大部分代码。这增加了应用程序的灵活性,使其更易于适应变化的需求。
7、支持异步编程
EF支持异步查询和保存操作,可以在需要时实现异步并发访问数据库,提高系统的响应性。
尽管EF提供了许多优势,但在一些性能敏感的场景中,原生ADO.NET仍然可能是更好的选择。开发人员需要根据项目的具体需求和性能要求来权衡选择使用EF还是原生ADO.NET。
25. 如何提高 LINQ 性能问题?
提高LINQ性能的关键在于编写高效的LINQ查询,避免不必要的开销和优化查询的执行计划。以下是一些提高LINQ性能的建议:
1、选择合适的数据结构
选择适合查询需求的数据结构,如使用字典(Dictionary)或哈希集合(HashSet)等,以提高查询性能。
2、使用合适的索引
如果查询涉及数据库,确保数据库表上的列有适当的索引。索引能够加速查询操作,特别是在涉及大量数据时。
3、延迟加载
使用延迟加载(延迟执行)的方式,只在需要时才执行查询。这可以通过使用 AsQueryable() 或 AsEnumerable() 来实现。
4、避免全表扫描
尽量避免在LINQ查询中进行全表扫描,尽量使用条件来缩小查询范围,以减少数据量。
5、分页查询
对于大数据集,考虑使用分页查询来限制每次返回的数据量。这可以通过使用 Skip 和 Take 方法来实现。
6、使用合适的投影
只选择需要的列,而不是选择整个对象。这可以通过使用 Select 进行投影,以减少返回的数据量。
7、优化查询语句
仔细编写LINQ查询,确保生成的查询语句是有效的。有时,手动编写LINQ查询可能比使用查询方法更灵活,并且能够更好地控制生成的SQL。
8、使用索引器
在内存中的集合中,如果可能,使用索引器(indexer)来直接访问元素,而不是进行线性搜索。
9、合理使用缓存
对于一些不经常变化的数据,可以考虑使用缓存来避免重复的查询操作。这适用于内存中的集合或某些数据存储中。
10、避免嵌套查询
尽量避免在查询中嵌套过多的子查询,因为这可能导致性能下降。可以考虑使用连接操作或者合并查询来优化。
11、了解查询执行计划
对于数据库查询,了解生成的SQL查询执行计划,使用数据库性能工具进行优化。
12、使用并行查询
在适当的情况下,可以考虑使用并行LINQ查询以提高性能,特别是对于大数据集。
通过综合考虑这些因素,可以有效提高LINQ查询的性能。在优化时,建议使用性能分析工具来评估和测试不同的优化策略的效果。
26. 什么是协变和逆变?
协变(Covariance)和逆变(Contravariance)是类型系统中的两个重要概念,通常涉及到泛型类型参数的关系。这两个概念分别描述了类型参数的子类型关系。
协变(Covariance):
协变发生在从派生类型到基础类型的转换时。如果一个泛型类型参数是协变的,那么可以将其派生类型作为实际类型的替代。在C#中,协变通常与 out 关键字关联,用于表示类型参数是输出的。
interface ICovariant<out T>
{
T GetItem();
}
class Example : ICovariant<string>
{
public string GetItem() => "Hello";
}
// 协变
ICovariant<object> covariantObj = new Example();
在上述例子中,ICovariant<out T> 接口中的 out 关键字表示类型参数 T 是协变的,所以 ICovariant<string> 可以隐式转换为 ICovariant<object>。
逆变(Contravariance):
逆变发生在从基础类型到派生类型的转换时。如果一个泛型类型参数是逆变的,那么可以将其基础类型作为实际类型的替代。在C#中,逆变通常与 in 关键字关联,用于表示类型参数是输入的。
interface IContravariant<in T>
{
void SetItem(T item);
}
class Example : IContravariant<object>
{
public void SetItem(object item) { /* implementation */ }
}
// 逆变
IContravariant<string> contravariantString = new Example();
在上述例子中,IContravariant<in T> 接口中的 in 关键字表示类型参数 T 是逆变的,所以 IContravariant<object> 可以隐式转换为 IContravariant<string>。
协变和逆变使得泛型类型在某些情况下更加灵活,特别是在处理委托、接口和代理等情况下。在设计泛型类型和接口时,可以根据实际需要使用协变和逆变,以提高代码的可复用性和灵活性。
27. 什么是 IEnumerable ?
IEnumerable
是 C# 中的一个接口,位于 System.Collections
命名空间,用于支持集合的迭代。该接口定义了一个方法 GetEnumerator()
,该方法返回一个实现了 IEnumerator
接口的对象,用于循环访问集合中的元素。
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
通过实现 IEnumerable
接口,你的类可以被用于 foreach
循环语句,从而能够被轻松地遍历。IEnumerable
接口通常用于非泛型集合。
在泛型集合的情况下,C# 提供了 IEnumerable<T>
接口,它继承自非泛型的 IEnumerable
接口,并且返回的枚举器是泛型的,不需要进行装箱和拆箱操作。
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
总体而言,IEnumerable
是 C# 中支持集合迭代的基础接口,使得开发人员能够使用统一的语法来遍历不同类型的集合。
28. IEnumerable 的缺点有哪些?
虽然 IEnumerable 接口在.NET中是一个灵活且强大的
枚举器接口,但也有一些缺点需要考虑:
1、只能前向遍历
IEnumerable 接口只支持前向遍历(单向迭代)。即,一旦开始遍历,就无法回到集合的开头或者在遍历过程中重新开始。
2、不支持并发修改
在使用 IEnumerable 进行枚举的过程中,不能在集合中执行添加、删除或修改操作。这样的并发修改会导致 InvalidOperationException 异常。
3、无法获取当前元素的索引
IEnumerable 接口没有提供直接获取当前元素索引的方法。如果需要索引,需要通过使用 for 循环或者通过转换为其他支持索引操作的接口来实现。
4、不提供集合大小信息
IEnumerable 不提供有关集合大小的信息,例如元素的数量。如果需要了解集合的大小,必须转换为其他接口或类型,如 ICollection 或 Count 属性。
5、无法进行过滤、投影等操作
IEnumerable 只提供了基本的迭代功能,不能直接进行过滤、投影等复杂的操作。对于这些功能,通常需要使用 LINQ 或者其他方法来处理。
6、性能问题
在一些特定场景下,使用 IEnumerable 进行遍历可能会导致性能问题,特别是在大数据集上。在需要更高性能的情况下,可能需要考虑其他集合接口或者数据结构。
虽然 IEnumerable 有一些限制,但它仍然是.NET中广泛使用的接口,因为它提供了一种通用的方式来处理集合。在实际应用中,开发人员需要根据具体场景和需求权衡使用 IEnumerable 的优势和缺点。如果需要更多的功能,可能需要考虑使用其他集合接口或实现。
29. 延迟执行 (Lazy Loading)是什么?
延迟执行(Lazy Loading)是一种编程模式,其中某些操作或计算在第一次需要时才被执行,而不是在一开始就立即执行。这模式通常用于优化性能和资源使用,以避免不必要的计算或加载。
在软件开发中,延迟执行可以应用于各种场景,包括数据加载、计算密集型操作、数据库查询等。以下是一些常见的延迟执行的应用:
1、实体属性的延迟加载
在对象关系映射(ORM)中,延迟加载常用于实体的属性。例如,在一个用户对象中,其关联的订单信息可能在用户首次访问订单属性时才被加载,而不是在用户对象创建时就立即加载。
2、数据库查询的延迟执行
在数据库访问中,延迟执行可以用于查询操作。查询可以被定义但在需要结果时才被执行,这样可以推迟对数据库的实际访问,直到数据确实被需要。
3、集合的延迟加载
在集合中,延迟加载可以用于推迟集合中的元素的加载。当需要访问集合中的元素时,才会实际加载这些元素。
在C#中,LINQ(Language Integrated Query)的查询操作通常是延迟执行的。LINQ查询会被定义,但只有在实际需要查询结果时才会执行。例如:
var query = from item in collection
where item.Property > 10
select item;
foreach (var result in query)
{
// 查询结果在此处被执行
}
在上述例子中,查询 query 的执行是延迟的,只有在 foreach 循环中实际迭代时才会执行查询。
延迟执行有助于优化性能和资源利用,因为它避免了在不需要结果时进行不必要的计算或加载。然而,需要小心使用延迟执行,确保它符合特定场景的需求。
30. LINQ 可视化工具简单介绍一下?
LINQPad工具是一个很好的LINQ查询可视化工具。它由Threading in C#和C# in a Nutshell的作者Albahari编写,完全免费。它的下载地址是[LINQPad](http://www.linqpad.net/)
进入界面后,LINQPad可以连接到已经存在的数据库(不过就仅限微软的SQL Server系,如果要连接到其他类型的数据库则需要安装插件)。某种程度上可以代替SQL Management Studio,是使用SQL Management Studio作为数据库管理软件的码农的强力工具,可以用于调试和性能优化(通过改善编译后的SQL规模)。
LINQPad支持使用SQL或C#语句(点标记或查询表达式)进行查询。你也可以通过点击橙色圈内的各种不同格式,看到查询表达式的各种不同表达方式:
Lambda:查询表达式的Lambda表达式版本,
SQL:由编译器转化成的SQL,通常这是我们最关心的部分,
IL:IL语言
LINQ可视化工具(LINQ Visualizer)是Visual Studio中的一个工具,用于可视化LINQ查询的执行结果。它提供了一个交互式的窗口,允许开发人员在调试LINQ查询时查看查询结果和中间步骤。
以下是LINQ可视化工具的简单介绍和使用方式:
1、打开LINQ可视化工具:
在Visual Studio中,可以在调试(Debugging)时使用LINQ可视化工具。在调试模式下,当遇到LINQ查询时,可以通过以下步骤打开LINQ可视化工具:
将鼠标悬停在LINQ查询变量上。
单击鼠标右键,在弹出的上下文菜单中选择“Quick Watch”或“Add Watch”。
在弹出的窗口中,查看LINQ Visualizer。
2、查看查询结果
LINQ可视化工具提供一个交互式的窗口,显示LINQ查询的执行结果。这包括查询中的每个步骤和最终结果。可以通过展开不同的节点查看中间步骤的数据。
3、支持不同视图
LINQ可视化工具支持不同的视图,包括表格视图、文本视图等,以便以不同的方式查看数据。可以根据需要切换视图。
4、逐步执行查询:
在LINQ可视化工具中,可以逐步执行LINQ查询,以了解每个步骤的执行情况。这有助于调试查询并理解查询的执行流程。
请注意,工具的确切功能和界面可能在不同版本的Visual Studio中略有变化。此外,可能有其他第三方工具或插件可用于更高级的LINQ查询调试和可视化
31. LINQ to Object 和 LINQ to SQL 有何区别?
LINQ to Objects 和 LINQ to SQL 是两种不同的LINQ提供程序,用于在.NET应用程序中查询不同的数据源。它们之间的主要区别在于它们所针对的数据源类型和工作原理。
LINQ to Objects:
数据源类型: LINQ to Objects 主要用于对内存中的对象集合进行查询。这可以包括数组、列表、集合等。它是LINQ的基础,支持对.NET集合和数组进行查询。
工作原理: LINQ to Objects在内存中对集合进行查询,不涉及数据库。它使用IEnumerable<T> 接口来扩展.NET集合,从而支持LINQ查询。查询结果是实时计算的,直接作用于内存中的集合。
LINQ to SQL:
数据源类型: LINQ to SQL 用于查询关系型数据库,主要是Microsoft SQL Server。它允许通过LINQ查询来操作数据库中的表、视图等。
工作原理: LINQ to SQL 利用实体-关系映射(Entity-Relational Mapping,ORM)的概念,将数据库表映射到.NET中的实体类。通过这种映射,开发人员可以使用LINQ查询来执行数据库操作。LINQ to SQL查询是通过生成的SQL语句在数据库中执行的。
主要区别总结如下:
1、数据源类型
LINQ to Objects 用于内存中的对象集合。
LINQ to SQL 用于关系型数据库。
2、工作原理
LINQ to Objects 在内存中对集合进行实时查询。
LINQ to SQL 通过生成的SQL语句在数据库中执行查询。
3、应用场景
LINQ to Objects 适用于对内存中的集合进行查询和操作。
LINQ to SQL 适用于与关系型数据库进行交互,执行数据库查询、更新、插入和删除等操作。
选择使用哪种LINQ提供程序取决于应用程序的数据源。如果数据存储在内存中的集合中,可以使用LINQ to Objects。如果数据存储在关系型数据库中,可以使用LINQ to SQL 或其他ORM框架。
32. 除了 EF,列举出你知道的 ORM 框架?
除了Entity Framework (EF),还有许多其他流行的ORM(Object-Relational Mapping)框架,用于简化对象与关系型数据库之间的映射和交互。以下是一些常见的ORM框架:
NHibernate:
NHibernate 是一个成熟的、开源的.NET平台上的ORM框架,它是Java平台上的Hibernate的.NET端口。NHibernate支持灵活的映射和强大的查询语言,并且有着广泛的社区支持。
Dapper:
Dapper 是一个轻量级、高性能的ORM框架,由Stack Overflow团队开发。它在性能上表现出色,适用于需要快速访问数据库的情况。Dapper不提供完整的对象映射,而是提供了一些简单的扩展方法来执行查询。
Fluent NHibernate:
Fluent NHibernate 是 NHibernate 的一个开源框架,它通过使用流畅的API(Fluent API)来简化NHibernate的映射配置。它提供了一种更直观的方式来定义对象到数据库表的映射关系。
LLBLGen Pro:
LLBLGen Pro 是一个商业级别的ORM框架,它支持多种数据库,并提供了强大的设计工具。LLBLGen Pro 允许开发人员通过可视化设计来定义数据模型,然后生成相关的代码。
Dapper Extensions:
Dapper Extensions 是 Dapper 的一个扩展,提供了一些额外的功能,如自动映射、关系映射等。它是基于Dapper构建的,保持了Dapper的简洁性和性能。
Topshelf:
Topshelf 是一个用于.NET的服务框架,可以方便地创建和部署Windows服务。虽然不是专门的ORM框架,但在某些场景下它可以与ORM框架一起使用,使开发者能够更容易地将应用程序部署为Windows服务。
PetaPoco:
PetaPoco 是一个轻量级的微ORM框架,用于.NET平台。它提供简单的API和高性能的查询,适用于小型和中型项目。
请注意,ORM框架的选择通常取决于项目的需求、开发者的偏好以及特定的性能和功能要求。每个框架都有其优点和适用场景,开发者应根据具体情况进行选择。
33. 如何如何获取 EF 生成的 Sql 脚本?
在Entity Framework (EF)中,你可以通过以下几种方式获取EF生成的SQL脚本:
通过DbContext的Database 属性:
使用DbContext的Database 属性,可以获取到Database对象,然后调用 GenerateCreateScript()、GenerateDropScript()、GenerateCreateScript() 等方法来生成相应的SQL脚本。
using (var context = new YourDbContext())
{
var createScript = context.Database.GenerateCreateScript();
var dropScript = context.Database.GenerateDropScript();
var migrateScript = context.Database.GenerateMigrateScript();
}
这样你就可以在代码中获取到相应的SQL脚本。
使用命令行工具:
Entity Framework Core 提供了命令行工具,可以使用 dotnet ef migrations script 命令来生成SQL脚本。例如:
dotnet ef migrations script -o output.sql
上述命令会生成迁移脚本并将其保存到名为 output.sql 的文件中。
在迁移中使用LogTo方法:
在迁移的Up 和 Down 方法中,你可以使用 LogTo 方法记录生成的SQL语句。例如:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("Your SQL Statement").LogTo(Console.WriteLine);
}
这将在控制台中输出生成的SQL语句。
选择适合你需求的方式,你可以在代码中获取或保存生成的SQL脚本。
34. 在哪些类型额项目中你会选择 EF? 为什么?
Entity Framework (EF)是一种强大的对象关系映射(ORM)框架,适用于各种类型的项目。选择是否使用EF通常取决于项目的需求、规模和开发团队的经验。以下是在哪些类型的项目中可能会选择使用EF的一些建议:
1、中小型应用程序
在中小型应用程序中,特别是对于快速开发和简化数据访问层的需求,EF是一个很好的选择。它提供了简单的API,允许开发人员通过对象模型而不是直接操作数据库来进行数据访问。
2、企业级应用程序
对于较大和复杂的企业级应用程序,EF提供了高度的灵活性和可维护性。通过使用Code First或Database First方法,可以轻松映射数据库模式和.NET对象模型,使开发人员能够专注于业务逻辑而不用过多关注数据库访问的细节。
3、ASP.NET应用程序
在ASP.NET应用程序中,EF可以轻松集成到MVC或Web API项目中。它与Entity Framework Core一样,支持异步查询,有助于提高Web应用程序的性能。
4、新项目和原型
对于新项目或原型,EF提供了快速开发的优势。使用Code First方法,开发人员可以通过定义实体类来快速创建数据库模式,而不必手动编写SQL脚本。
5、团队经验和技能
如果开发团队对EF有较高的熟悉度和经验,且能够有效地使用其功能,则EF是一个强大的工具。团队成员之间的一致性和共享的经验有助于提高开发效率。
6、支持LINQ查询
如果项目需要使用LINQ进行强类型的查询,EF是一个理想的选择。它允许开发人员使用LINQ语法而不是传统的SQL语句,提高了查询的可读性和维护性。
请注意,虽然EF在许多项目中都是一个优秀的选择,但在某些情况下,特别是对于对性能要求非常高、需要更细粒度控制的项目,也可以考虑其他ORM框架或直接使用ADO.NET。最终的选择取决于项目的具体需求和开发团队的技能水平。
35. 请说明 EF 中映射实体对象的几种状态?
在Entity Framework (EF)中,实体对象可以处于不同的状态,这些状态描述了对象在上下文中的状态和对数据库的影响。EF中的实体状态包括以下几种:
1、Unchanged(未更改)
当一个实体对象从数据库中查询出来或者通过上下文追踪到时,它的状态被标记为未更改。这表示实体的属性值与数据库中的值相匹配,没有任何更改。
2、Added(已添加)
当通过上下文的 Add 方法向数据库中插入新的实体时,该实体的状态被标记为已添加。这表示该实体是一个新对象,尚未存在于数据库中。
3、Modified(已修改)
当实体的属性值发生更改,并且通过上下文的 SaveChanges 方法提交这些更改时,实体的状态被标记为已修改。这表示实体的属性值与数据库中的值不同。
4、Deleted(已删除)
当通过上下文的 Remove 方法删除数据库中的实体时,实体的状态被标记为已删除。这表示该实体将在提交更改后从数据库中删除。
5、Detached(分离)
当实体对象被创建但未被上下文跟踪,或者实体在上下文中被删除后,其状态被标记为分离。分离的实体不再受上下文的跟踪,对其进行的更改不会影响数据库。
在使用EF进行数据操作时,了解实体对象的状态是很重要的。开发人员可以通过查看 Entry 属性来获取实体的状态,并据此采取相应的操作。例如:
var entry = context.Entry(entity);
if (entry.State == EntityState.Modified)
{
// 处理已修改的实体
}
else if (entry.State == EntityState.Added)
{
// 处理已添加的实体
}
// 其他状态的处理
这种状态管理有助于开发人员了解哪些实体发生了更改,并在保存更改时采取适当的操作。
36. 如果实体名称和数据库表名不一致,该如何处理?
如果实体名称与数据库表名不一致,Entity Framework (EF)提供了多种方式来处理这种映射不一致的情况。以下是一些常见的处理方式:
1、使用数据注解(Data Annotations)
可以使用数据注解来显式指定实体对应的表名。在实体类上使用 [Table] 属性,并传入数据库中的表名。
[Table("YourTableName")]
public class YourEntity
{
// 实体属性
}
2、使用Fluent API
在DbContext的OnModelCreating方法中,使用Fluent API配置实体与表之间的映射关系。通过 ToTable 方法指定实体对应的表名。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<YourEntity>().ToTable("YourTableName");
}
3、约定(Convention)
EF使用一些默认的约定来进行映射,其中之一是将实体名称映射到与其相同的表名。如果实体名称符合默认约定,不需要额外的配置,EF会自动进行映射。
public class YourEntity
{
// 实体属性
}
// 上述实体默认映射到数据库表名为 "YourEntities"。
选择合适的方式取决于项目的具体需求和团队的偏好。通常,建议使用数据注解或Fluent API进行显式配置,以确保更灵活和明确的映射关系。
37. 泛型的优点有哪些?
泛型在编程中具有许多优点,它们提供了一种通用的、类型安全的代码抽象机制。以下是泛型的一些优点:
1、代码复用
泛型允许编写通用的、与类型无关的代码,这样一份代码可以在不同的数据类型上进行重复使用,而不需要为每种数据类型编写专门的代码。这提高了代码的复用性。
2、类型安全
使用泛型能够在编译时捕获类型错误,而不是在运行时。这提高了代码的可靠性和可维护性,因为在编写代码时就能够发现并解决潜在的类型相关问题。
3、性能提升
泛型是在编译时实现的,而不是在运行时进行类型检查和转换。这意味着泛型代码通常比非泛型代码更加高效,因为它避免了运行时的装箱和拆箱操作。
4、可读性和可维护性
泛型使得代码更加抽象和通用,减少了冗余的代码。这提高了代码的可读性,并使代码更易于理解和维护。通过泛型,可以更清晰地表达算法或数据结构,而不受到特定类型的限制。
5、安全性和稳定性
泛型提供了一种安全的、类型检查的方式来处理不同数据类型,减少了由于类型不匹配而引起的运行时错误。这使得应用程序更加健壮和稳定。
6、灵活性
泛型使得代码更加灵活,能够适应不同类型和数据结构的需求。通过泛型,可以创建通用的库和框架,以应对不同场景的需求。
7、容器和集合
泛型在容器和集合类中得到广泛应用,如List<T>、Dictionary<K, V>等。这使得操作和管理数据集合变得更加方便和类型安全。
总体而言,泛型提供了一种强大的编程机制,使得代码更加通用、类型安全、高效,并且提高了代码的可读性和可维护性。
38. try {} 里有一个 return 语句,那么紧跟在这个 try 后的 finally {} 里的 code 会不会被执行,什么时候被执行,在 return 前还是后?
在C#中,finally块中的代码总是在try块中的return语句之前执行。即使try块中存在return语句,finally块中的代码也会在函数或方法返回之前执行。这确保了finally块中的清理代码在任何情况下都会得到执行。
// 以下是一个示例说明:
public int ExampleMethod()
{
try
{
// 可能的操作
return 42; // 在这里遇到return语句
}
finally
{
// 在return语句之前执行的finally块
// 可以包含清理代码等
Console.WriteLine("Finally block is executed.");
}
}
在上述示例中,即使try块中存在return语句,finally块中的代码仍然会在函数返回之前执行。这是为了确保在函数返回之前执行必要的清理操作,以及处理异常情况下的资源释放等操作。
39. C# 中的异常类有哪些?
在C#中,异常类是通过 System.Exception 类派生出来的。以下是一些常见的异常类,它们都是 System.Exception 的派生类:
System.Exception:
所有异常的根类,其他所有异常类都是直接或间接地继承自它。
System.SystemException:
用于表示由运行时操作引发的异常的基类。
System.ApplicationException:
用于表示应用程序定义的异常的基类。
System.NullReferenceException:
在试图访问引用对象的成员或方法时,如果对象引用为 null,则引发此异常。
System.IndexOutOfRangeException:
在尝试访问数组中不存在的索引时引发的异常。
System.ArgumentException:
当传递给方法的参数不满足方法的预期条件时引发的异常。
System.InvalidOperationException:
在对象的当前状态下不支持调用方法或操作时引发的异常。
System.DividedByZeroException:
在试图除以零时引发的异常。
System.ArithmeticException:
所有算术运算异常的基类。
System.FormatException:
当字符串格式不符合要求时,尝试进行格式化或解析操作时引发的异常。
System.NotSupportedException:
当调用不支持的方法或操作时引发的异常。
System.TimeoutException:
在操作超时时引发的异常。
System.IO.IOException:
所有IO异常的基类。
System.Net.WebException:
在与网络相关的操作中发生错误时引发的异常。
System.Xml.XmlException:
在XML文档的读取或解析过程中发生错误时引发的异常。
这只是一些常见的异常类,实际上还有许多其他异常类,每个异常类都表示一种特定类型的错误或异常情况。开发人员可以通过捕获这些异常来处理错误,保护应用程序免受潜在的问题。
40. 泛型有哪些常见的约束?
在C#中,泛型约束是用于指定泛型类型参数必须满足的条件。这些约束有助于提高泛型代码的类型安全性和灵活性。以下是常见的泛型约束:
1、where T : class
// T必须是引用类型。这排除了值类型,使得泛型类型参数必须是类、接口、委托或数组。
public class Example<T> where T : class
{
// 泛型类的代码
}
2、where T : struct
// T必须是值类型。这排除了引用类型,使得泛型类型参数必须是结构体。
public class Example<T> where T : struct
{
// 泛型类的代码
}
3、where T : new()
// T必须具有无参数的公共构造函数。这允许在泛型类或方法中使用 new T() 来创建泛型类型的实例。
public class Example<T> where T : new()
{
// 泛型类的代码
}
4、where T : <base class>
// T必须是指定的基类或派生自指定的基类。
public class Example<T> where T : MyBaseClass
{
// 泛型类的代码
}
5、where T : <interface>:
// T必须实现指定的接口。
public class Example<T> where T : IMyInterface
{
// 泛型类的代码
}
6、where T : U
// T必须派生自或实现U。
public class Example<T, U> where T : U
{
// 泛型类的代码
}
7、where T : enum
// T必须是枚举类型。
public class Example<T> where T : Enum
{
// 泛型类的代码
}
8、where T : unmanaged
// T必须是非托管类型。这通常用于对原始数据进行操作,如指针。
public class Example<T> where T : unmanaged
{
// 泛型类的代码
}
这些泛型约束可以单独或组合使用,以便更精确地指定泛型类型参数的要求。使用泛型约束可以使泛型代码更灵活、安全,并提高代码的可读性。
41. Collection 和 Collections 的区别?
在C#中,Collection 和 Collections 是两个不同的名称,它们可能是指具体的类、命名空间或其他程序中的标识符。一般情况下,这两个名称并没有特定的含义,因此需要根据上下文来确定其具体指代的内容。
如果是指的是 System.Collections 命名空间,它是 .NET Framework 提供的包含许多集合类的命名空间。这些集合类包括 List<T>、Dictionary<K, V>、Queue<T>、Stack<T> 等。在这种情况下,Collections 是 Collection 的复数形式,表示一组集合类。
using System.Collections;
// 示例使用 System.Collections 命名空间中的集合类
ArrayList myCollection = new ArrayList();
如果是指的是具体的 Collection 类,那么需要查看具体上下文中的定义。可能是某个自定义类或框架中的特定集合类。
// 示例使用自定义的 Collection 类
MyCustomCollection myCollection = new MyCustomCollection();
总的来说,需要根据具体的上下文和代码来确定 Collection 和 Collections 的确切含义。如果能够提供更多的上下文或代码片段,我可以提供更详细的解释。
42. 能用 foreach 遍历访问的对象的要求?
使用 foreach 遍历访问对象的要求是对象必须实现 IEnumerable 或 IEnumerable<T> 接口。这两个接口提供了用于迭代集合的标准方法。具体要求如下:
实现 IEnumerable 接口:
对象需要实现 IEnumerable 接口,该接口定义了一个方法 GetEnumerator(),返回一个实现 IEnumerator 接口的对象。IEnumerator 接口包含了用于遍历集合的 MoveNext() 和 Current 方法。
public class MyCollection : IEnumerable
{
// 实现 IEnumerable 接口
public IEnumerator GetEnumerator()
{
// 返回一个实现 IEnumerator 接口的对象
return new MyEnumerator();
}
}
或实现 IEnumerable<T> 接口:
如果对象是泛型集合,可以实现 IEnumerable<T> 接口。该接口继承自 IEnumerable,并提供类型安全的迭代方法。
public class MyGenericCollection<T> : IEnumerable<T>
{
// 实现 IEnumerable<T> 接口
public IEnumerator<T> GetEnumerator()
{
// 返回一个实现 IEnumerator<T> 接口的对象
return new MyGenericEnumerator<T>();
}
// 实现 IEnumerable 接口(非泛型)
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
只有实现了上述接口的对象,才能够通过 foreach 循环进行遍历。这是因为 foreach 在遍历集合时依赖于这两个接口提供的标准遍历方法。如果对象未实现这些接口,编译器将会报错。
43. 说出五个集合类?
在C#中,有许多集合类,以下是其中的五个常用的集合类:
1、List<T>
List<T> 是一个动态数组实现,它允许在列表末尾高效地添加和删除元素。它提供了按索引访问元素的功能,是一个通用的动态数组。
List<int> numbers = new List<int>();
2、Dictionary<TKey, TValue>
Dictionary<TKey, TValue> 是一个键值对集合,提供了基于键快速查找值的功能。它实现了哈希表,使得查找、添加和删除操作都非常高效。
Dictionary<string, int> ages = new Dictionary<string, int>();
3、Queue<T>
Queue<T> 是一个先进先出(FIFO)的队列实现,允许在队列的尾部添加元素,在队列的头部删除元素。它常用于实现队列数据结构。
Queue<string> queue = new Queue<string>();
4、Stack<T>
Stack<T> 是一个后进先出(LIFO)的栈实现,允许在栈的顶部添加元素,并从栈的顶部删除元素。它常用于实现栈数据结构。
Stack<double> stack = new Stack<double>();
5、HashSet<T>
HashSet<T> 是一个不包含重复元素的集合实现,它使用哈希表来提供快速的查找和插入操作。它实现了集合的数学概念,不允许包含重复的元素。
HashSet<int> uniqueNumbers = new HashSet<int>();
这些集合类提供了丰富的功能和灵活性,可以根据具体的需求选择合适的集合类。它们都位于 System.Collections.Generic 命名空间下。
44. HashMap 和 Hashtable 区别?
Collection是集合类的上级接口,Collections是针对集合类的一个帮助类,它提供一系列静态方法来实现对各种集合的搜索,排序,线程安全化操作。
在C#中,HashMap 和 Hashtable 是两个用于存储键值对的集合类,但它们有一些区别。在C#中,通常使用 Dictionary<TKey, TValue> 作为 HashMap 的替代,而 Hashtable 是在 .NET Framework 1.1 之前引入的旧的集合类。
以下是它们之间的一些区别:
1、泛型 vs 非泛型
HashMap 通常指的是使用泛型的 Dictionary<TKey, TValue>。它提供了类型安全的键值对存储。
Hashtable 是一个非泛型的集合类,它存储的键和值都是 object 类型。这可能导致装箱和拆箱的性能开销,并且在使用时需要进行类型转换。
2、类型安全
HashMap 是类型安全的,因为它使用泛型来指定键和值的类型。这意味着在编译时可以捕获类型错误。
Hashtable 是非类型安全的,因为它存储的键和值都是 object 类型,需要在运行时进行类型检查和转换。
3、Null 键和值
HashMap 允许使用 null 作为键和值。
Hashtable 不允许使用 null 作为键或值,如果尝试将 null 作为键或值添加到 Hashtable 中,会引发 ArgumentNullException。
4、线程安全性
HashMap(Dictionary<TKey, TValue>)是非线程安全的,如果在多个线程同时修改,需要进行外部同步。
Hashtable 是线程安全的,可以在多个线程中安全地使用。如果需要在多线程环境中使用 Dictionary<TKey, TValue>,可以使用 ConcurrentDictionary<TKey, TValue>。
5、性能
由于 HashMap 使用泛型和提供类型安全,通常在性能上优于 Hashtable。
Hashtable 是一个旧的集合类,对于新的代码,推荐使用 Dictionary<TKey, TValue>。
总的来说,HashMap(Dictionary<TKey, TValue>)是更现代、更安全和更高性能的选择,而 Hashtable 主要是为了向后兼容而保留的旧的集合类。在新的代码中,建议使用 Dictionary<TKey, TValue>。
本系列文章题目摘自网络,答案重新梳理
- 点赞
- 收藏
- 关注作者
评论(0)