Những câu hỏi phỏng vấn C#/.Net (Senior)
72. Phạm vi của biến thành viên Protected Internal trong một lớp C# là gì?
Protected internal access specifier cho phép một lớp ẩn các biến thành viên và các hàm thành viên của nó khỏi các đối tượng và hàm của lớp khác, ngoại trừ một lớp con trong cùng một ứng dụng. Điều này cũng được sử dụng khi thực hiện kế thừa.
class access
{
// String Variable declared as protected internal
protected internal string name;
public void print()
{
Console.WriteLine("\nMy name is " + name);
}
}
73. Khi nào sử dụng IEnumerable thay vì List, và chúng hoạt động như thế nào?
IEnumerable mô tả hành vi, còn List là một triển khai của hành vi đó. Khi bạn sử dụng IEnumerable, bạn cho phép trình biên dịch trì hoãn hoạt động cho đến sau này, có thể sẽ tối ưu hóa trong quá trình thực hiện. Nếu bạn sử dụng ToList(), bạn buộc trình biên dịch phải thay đổi kết quả ngay lập tức.
Bất cứ khi nào bạn “xếp chồng” các biểu thức LINQ, bạn nên sử dụng IEnumerable, bởi vì chỉ xác định hành vi cho phép LINQ có cơ hội trì hoãn việc đánh giá và có thể tối ưu hóa chương trình.
74. Constructor Chaining trong C# là gì?
Constructor Chaining là một cách tiếp cận mà một phương thức khởi tạo (constructor) gọi một phương thức khởi tạo khác trong cùng một lớp cơ sở hoặc lớp cơ sở. Điều này rất hữu ích khi chúng ta có một lớp định nghĩa nhiều phương thức khởi tạo.
class Foo {
private int id;
private string name;
public Foo() : this(0, "") {
}
public Foo(int id) : this(id, "") {
}
public Foo(string name) : this(0, name) {
}
public Foo(int id, string name) {
this.id = id;
this.name = name;
}
}
75. Một phương thức có thể được nạp chồng theo những cách nào?
Các phương thức có thể được nạp chồng (overload) bằng cách sử dụng các kiểu dữ liệu khác nhau cho tham số, thứ tự khác nhau của tham số và số lượng tham số khác nhau.
76. Giải thích sự khác biệt giữa Select và Where?
Vấn đề:
ContextSet().Select(x=> x.FirstName == "John") // A
ContextSet().Where(x=> x.FirstName == "John") // B
Khi nào chúng ta nên sử dụng .Select thay vì .Where?
Giải pháp:
- Select là một phép chiếu, vì vậy những gì bạn nhận được là biểu thức x => x.FirstName == “John” được ước tính cho mỗi phần tử trong ContextSet() trên máy chủ, tức là rất nhiều giá trị true/false (cùng số lượng với danh sách ban đầu của bạn). Nếu bạn nhìn vào vùng chọn sẽ trả về một cái gì đó giống như IEnumerable<bool> (vì kiểu của x => x.FirstName == “John” là bool). Sử dụng Select khi bạn muốn giữ tất cả các kết quả, nhưng thay đổi kiểu của chúng (chiếu chúng).
- Where lọc các kết quả, trả về một kiểu dữ liệu ban đầu (không có phép chiếu). Sử dụng Where khi bạn muốn lọc kết quả của mình, giữ nguyên kiểu ban đầu.
77. Sự khác biệt giữa phương thức dispose và finalize là gì trong C#?
Cả hai Finalize và Dispose đều được sử dụng cho cùng một nhiệm vụ giải phóng unmanaged resource (tài nguyên không được quản lý) nhưng có một số điểm khác biệt.
Finalize:
- Finalize được sử dụng để giải phóng các unmanaged resource không được sử dụng như tệp, kết nối cơ sở dữ liệu trong miền ứng dụng, … được giữ bởi một đối tượng trước khi đối tượng đó bị phá hủy.
- Trong quy trình nội bộ (Internal process), nó được gọi bởi Garbage Collector và không thể gọi thủ công bằng mã người dùng hoặc bất kỳ service nào.
- Finalize thuộc về lớp System.Object.
- Triển khai nó khi bạn có unmanaged resource trong mã của mình và đảm bảo rằng những tài nguyên này được giải phóng khi quá trình thu gom rác diễn ra.
Dispose:
- Dispose cũng được sử dụng để giải phóng các unmanaged resource không được sử dụng như tệp, kết nối cơ sở dữ liệu trong miền ứng dụng bất cứ lúc nào.
- Dispose được gọi thủ công bằng mã người dùng một cách rõ ràng.
- Nếu chúng ta cần phương thức dispose thì phải triển khai lớp đó bằng IDisposable interface.
- Dispose thuộc về IDisposable interface.
- Triển khai dispose khi bạn đang viết một custom class để được sử dụng bởi những người dùng khác.
78. Indexer trong C# là gì?
Indexer là một tiện ích cú pháp cho phép bạn tạo một class, struct hoặc interface mà các ứng dụng client có thể truy cập dưới dạng một array.
Để khai báo một indexer trên một class hoặc struct, hãy sử dụng từ khóa this:
// Indexer declaration
public int this[int index] {
// get and set accessors
}
Sau đó, bạn có thể truy cập instance của class này bằng cách sử dụng toán tử truy cập mảng []. Các indexer có thể được overload (nạp chồng). Các indexer có thể được khai báo với nhiều tham số và mỗi tham số có thể là một kiểu khác nhau. Không nhất thiết các index phải là số nguyên. C# cho phép các index thuộc các kiểu khác, ví dụ, một string.
class IndexedNames {
private string[] namelist = new string[size];
public string this[int index] {
get {
string tmp;
if (index >= 0 && index <= size - 1) {
tmp = namelist[index];
} else {
tmp = "";
}
return (tmp);
}
set {
if (index >= 0 && index <= size - 1) {
namelist[index] = value;
}
}
}
}
79. Bạn có thể tạo một hàm trong C# có thể chấp nhận số lượng đối số khác nhau không?
Bằng cách sử dụng từ khóa params, bạn có thể chỉ định một tham số của phương thức nhận một số lượng đối số thay đổi. Không có tham số bổ sung nào khác được phép sau từ khóa params trong khai báo phương thức và chỉ một từ khóa params được phép trong khai báo phương thức. Kiểu khai báo của tham số params phải là mảng một chiều.
public static void UseParams(params int[] list) {
for (int i = 0; i < list.Length; i++) {
Console.Write(list[i] + " ");
}
Console.WriteLine();
}
public static void UseParams2(params object[] list) {
for (int i = 0; i < list.Length; i++) {
Console.Write(list[i] + " ");
}
Console.WriteLine();
}
// usage
UseParams(1, 2, 3, 4);
UseParams2(1, 'a', "test");
80. Khi nào bạn sẽ sử dụng các delegate trong C#?
Bây giờ chúng ta có các biểu thức lambda và các phương thức anonymous (ẩn danh) trong C#, chúng ta sử dụng các delegate nhiều hơn nữa. Trong C# 1, ở đó bạn luôn phải có một phương thức riêng để triển khai logic, việc sử dụng một delegate thường không có ý nghĩa. Ngày nay, chúng ta sử dụng delegate cho:
- Event handlers (dành cho GUI và các thứ khác).
- Start các thread.
- Callbacks (ví dụ: đối với API bất đồng bộ).
- LINQ và tương tự (List.Find, v.v.).
- Bất kỳ nơi nào khác mà chúng ta muốn áp dụng mã “mẫu” một cách hiệu quả với một số logic chuyên dụng bên trong (nơi delegate cung cấp chuyên môn).
81. Tại sao sử dụng câu lệnh lock trong C#?
Từ khóa lock đảm bảo rằng một thread không tiến vào một khu vực mã quan trọng khi đang có một thread khác nằm trong đó. Nếu một thread khác cố gắng tiến vào mã bị lock, nó sẽ đợi, chặn, cho đến khi đối tượng được giải phóng.
Từ khóa lock gọi Enter ở đầu khối và Exit ở cuối khối. Từ khóa lock thực sự xử lý lớp Monitor ở phía sau.
private static readonly Object obj = new Object();
lock (obj)
{
// khu vực mã quan trọng
}
82. Marshalling là gì và tại sao chúng ta cần nó?
Bởi vì các ngôn ngữ và môi trường khác nhau có các quy ước gọi khác nhau, các quy ước bố cục khác nhau, các kích thước primitive khác nhau (ví dụ char trong C# và char trong C), các quy ước tạo / hủy đối tượng khác nhau và các hướng dẫn thiết kế khác nhau. Bạn cần một cách để đưa những thứ từ môi trường được quản lý vào một nơi nào đó mà môi trường không được quản lý có thể nhìn thấy và hiểu nó và ngược lại. Đó là những gì mà Marshalling thực hiện.
Marshaling là quá trình tạo cầu nối giữa managed code và unmanaged code, Marshalling là một trong những dịch vụ quan trọng nhất do CLR cung cấp.
83. Có thể thực hiện đa kế thừa trong C# không?
Trong C#, các lớp kế thừa chỉ có thể kế thừa từ một lớp cơ sở. Nếu bạn muốn kế thừa từ nhiều lớp cơ sở, hãy sử dụng interface.
84. Các tác vụ bất đồng bộ Async/Await hoạt động như thế nào trong .NET?
Vấn đề:
private async Task<bool> TestFunction()
{
var x = await DoesSomethingExists();
var y = await DoesSomethingElseExists();
return y;
}
Câu lệnh await thứ hai có được thực thi ngay lập tức hay là sau khi câu lệnh await đầu tiên trả về?
Giải pháp:
await tạm dừng phương thức cho đến khi hoạt động hoàn tất. Vì vậy, await thứ hai sẽ được thực thi sau khi await đầu tiên trả về.
Mục đích của await là nó sẽ trả lại thread hiện tại cho thread pool (nhóm thread) khi hoạt động được await kết thúc và thực hiện bất cứ điều gì.
Điều này đặc biệt hữu ích trong các môi trường hiệu suất cao, chẳng hạn như một máy chủ web, nơi một request được xử lý trên một thread nhất định từ thread pool tổng thể. Nếu chúng ta không sử dụng await, thì thread đã cho xử lý request (và tất cả tài nguyên của nó) vẫn “được sử dụng” khi lệnh gọi database / service hoàn tất. Quá trình này có thể mất vài giây hoặc hơn, đặc biệt là đối với các cuộc gọi service bên ngoài.
85. Bạn hãy giải thích sự khác biệt giữa interface, abstract class, sealed class, static class và partial class trong C#?
- abstract class: nên được sử dụng khi có một mối quan hệ IS-A và không có instance nào được phép tạo từ abstract class đó. Ví dụ: một Animal là một lớp cơ sở abstract, nơi các loài động vật cụ thể có thể được kế thừa, ví dụ là Horse, Pig, v.v. Bằng cách tạo ra lớp abstract Animal, chúng ta không được phép tạo ra một instance của Animal.
- interface: một interface nên được sử dụng để thực hiện chức năng trong một lớp. Giả sử chúng ta muốn một con ngựa có thể Jump, một IJumping interface có thể được tạo ra. Bằng cách thêm interface này vào Horse, tất cả các phương thức trong IJumping sẽ được triển khai. Trong riêng IJumping, chỉ có các khai báo (ví dụ: StartJump và EndJump), trong Horse, việc triển khai hai phương thức này phải được thêm vào.
- sealed class: bằng cách làm niêm phong Horse, không lớp nào có thể kế thừa từ nó, ví dụ: thực hiện cho các lớp như Pony hoặc Workhorse mà bạn muốn được kế thừa từ Horse.
- static class: chủ yếu được sử dụng cho các chức năng ‘tiện ích’. Giả sử bạn cần một số phương thức để tính toán trung bình của một số được sử dụng trong lớp Horse, nhưng bạn không muốn đặt nó trong Horse vì nó không liên quan và nó cũng không liên quan đến động vật, bạn có thể tạo một lớp để loại phương thức này vào trong. Bạn không cần một instance của một lớp tiện ích như vậy.
- partial class: một partial class không gì khác hơn là phân tách tệp của một lớp thành nhiều tệp nhỏ hơn. Một lý do để làm điều này có thể là để chia sẻ một phần của mã nguồn cho người khác. Nếu lý do là tệp quá lớn, hãy nghĩ về việc chia lớp đó thành các lớp khác nhỏ hơn trước.
86. Cách sử dụng nào là tốt nhất khi sử dụng các đối tượng Lazy?
Bạn thường sử dụng nó khi bạn muốn khởi tạo một thứ gì đó ở lần đầu tiên nó được sử dụng. Điều này chỉ làm chúng ta tốn chi phí khi tạo ra nó thay vì luôn luôn phát sinh chi phí.
Thông thường điều này được ưu tiên sử dụng khi đối tượng có thể hoặc không thể được sử dụng và chi phí khởi taọ nó là không nhỏ.
Ví dụ: Lazy<T> giúp việc triển khai các singleton lazy và thread-safe dễ dàng:
public sealed class Singleton
{
// Because Singleton's constructor is private, we must explicitly
// give the Lazy<Singleton> a delegate for creating the Singleton.
static readonly Lazy<Singleton> instanceHolder =
new Lazy<Singleton>(() => new Singleton());
Singleton()
{
// Explicit private constructor to prevent default public constructor.
...
}
public static Singleton Instance => instanceHolder.Value;
}
87. Sự khác biệt giữa toán tử is và as trong C# là gì?
- Toán tử is kiểm tra xem một đối tượng có thể được ép thành một kiểu cụ thể hay không.
if (someObject is StringBuilder) ...
- Toán tử as cố gắng ép một đối tượng thành một kiểu cụ thể và trả về null nếu nó không thành công.
StringBuilder b = someObject as StringBuilder;
if (b != null) ...
88. Từ khóa yield được sử dụng để làm gì trong C#?
Vấn đề:
IEnumerable<object> FilteredList()
{
foreach( object item in FullList )
{
if( IsItemInPartialList( item )
yield return item;
}
}
Bạn có thể giải thích từ khóa yield làm gì ở trong đoạn mã trên không?
Giải pháp:
Hàm trên trả về một đối tượng thực thi IEnumerable interface. Nếu một hàm đang gọi bắt đầu foreach-in qua đối tượng này thì hàm được gọi lại cho đến khi nó “yield”. Đây là cú pháp được giới thiệu trong C# 2.0.
yield có hai cách sử dụng tuyệt vời:
- Nó giúp cung cấp sự lặp tùy chỉnh mà không cần tạo các collection tạm.
- Nó giúp thực hiện lặp trạng thái.
Lợi thế của việc sử dụng yield là nếu hàm sử dụng dữ liệu của bạn chỉ cần item đầu tiên của collection, thì item còn lại sẽ không được tạo.
Một ví dụ khác:
public void Consumer()
{
foreach(int i in Integers())
{
Console.WriteLine(i.ToString());
}
}
public IEnumerable<int> Integers()
{
yield return 1;
yield return 2;
yield return 4;
yield return 8;
yield return 16;
yield return 16777216;
}
Khi bạn xem qua ví dụ này, bạn sẽ thấy lệnh gọi đầu tiên đến Integers() trả về 1. Lệnh gọi thứ hai trả về 2 và dòng “yield return 1” không được thực thi lại.
89. Các kiểu con trỏ trong C# là gì?
Các biến kiểu con trỏ (pointer type) lưu trữ địa chỉ bộ nhớ của một kiểu khác. Con trỏ trong C# có cùng khả năng với con trỏ trong C hoặc C++.
90. Sự khác biệt giữa lớp System.ApplicationException và lớp System.SystemException là gì?
- Lớp System.ApplicationException hỗ trợ các exception do các chương trình ứng dụng tạo ra. Do đó, các exception được định nghĩa bởi các lập trình viên nên kế thừa từ lớp này.
- Lớp System.SystemException là lớp cơ sở cho tất cả các exception hệ thống được xác định trước.
91. Sự khác biệt giữa late binding và early binding trong C#?
- Trong Compile time polymorphism (đa hình thời gian biên dịch) hay gọi là Early Binding (liên kết sớm), chúng ta sẽ sử dụng nhiều phương thức có cùng tên nhưng khác tham số, bởi vì điều này mà chúng ta có thể thực hiện các tác vụ khác nhau với cùng một tên phương thức trong cùng một lớp và nó được gọi là Method overloading.
- Run time polymorphism (đa hình thời gian chạy) còn được gọi là Late Binding (liên kết muộn). Trong Late Binding, chúng ta có thể sử dụng cùng một tên phương thức với cùng chữ ký có nghĩa là cùng kiểu hoặc cùng số lượng tham số nhưng không cùng lớp vì trình biên dịch không cho phép điều đó tại thời điểm biên dịch, vì vậy chúng ta có thể sử dụng trong lớp kế thừa liên kết tại runtime khi một đối tượng của lớp con hoặc lớp kế thừa được khởi tạo, đó là cách chúng ta nói rằng Late Binding còn được gọi là Method overriding.
92. Sự khác biệt giữa Func<string,string> và delegate?
Vấn đề:
Func<string, string> convertMethod = lambda; // A
public delegate string convertMethod(string value); // B
Cả hai đều là delegate? Chúng có gì khác biệt không?
Giải pháp:
Đầu tiên là khai báo một biến delegate và gán giá trị cho nó, thứ hai là chỉ định nghĩa một kiểu delegate. Cả Func<string,string> và delegate string convertMethod(string) sẽ có khả năng giữ các định nghĩa phương thức giống nhau cho dù chúng là phương thức, phương thức anonymous hay biểu thức lambda.
public static class Program
{
// you can define your own delegate for a nice meaningful name, but the
// generic delegates (Func, Action, Predicate) are all defined already
public delegate string ConvertedMethod(string value);
public static void Main()
{
// both work fine for taking methods, lambdas, etc.
Func<string, string> convertedMethod = s => s + ", Hello!";
ConvertedMethod convertedMethod2 = s => s + ", Hello!";
}
}
93. Làm thế nào để giải quyết vấn đề Circular Reference?
Vấn đề:
Làm thế nào để bạn giải quyết các vấn đề circular reference, ví dụ như lớp A có lớp B là một trong những thuộc tính của nó, trong khi lớp B có lớp A là một trong những thuộc tính của nó?
Giải pháp:
Đầu tiên tôi sẽ nói bạn cần phải suy nghĩ lại về thiết kế của bạn. Circular reference như bạn mô tả thường là một lỗ hổng thiết kế. Trong hầu hết các trường hợp, khi tôi phải có hai thứ tham chiếu lẫn nhau, tôi đã tạo một interface để xóa circular reference này. Ví dụ:
TRƯỚC
public class Foo
{
Bar myBar;
}
public class Bar
{
Foo myFoo;
}
Biểu đồ phụ thuộc:
Foo phụ thuộc vào Bar, nhưng Bar cũng phụ thuộc vào Foo. Nếu chúng nằm trong các assembly riêng biệt, bạn sẽ gặp vấn đề về việc build code(ví dụ như Build Solution trong Visual Studio), đặc biệt nếu bạn thực hiện việc Clean Solution -> Build Solution.
SAU
public interface IBar
{}
public class Foo
{
IBar myBar;
}
public class Bar : IBar
{
Foo myFoo;
}
Biểu đồ phụ thuộc:
Cả Foo và Bar đều phụ thuộc vào IBar. Không có circular dependency, và nếu IBar được đặt trong assembly của chính nó, Foo và Bar đang trong các assembly riêng biệt thì cũng không có vấn đề gì.
94. Khi nào sử dụng ArrayList thay vì array[] trong C#?
- Array là strongly typed và hoạt động tốt như các tham số. Nếu bạn biết độ dài của collection của mình và nó cố định, bạn nên sử dụng một array.
- ArrayList không phải là strongly typed, mọi Insertion hoặc Retrial (truy xuất) sẽ cần một chuyển đổi (cast) để quay trở lại kiểu ban đầu của bạn. Nếu bạn cần một phương thức để lấy một danh sách của một kiểu cụ thể, thì ArrayList sẽ ngắn hơn vì bạn có thể truyền vào ArrayList bất kỳ kiểu nào. ArrayList sử dụng một mảng tự động mở rộng bên trong, nghĩa là nó sẽ tự động thay đổi kích thước khi bạn thêm/xóa phần tử trong ArrayList.
Những gì bạn thực sự muốn sử dụng là một generic list như List.
List có tất cả các lợi thế của Array và ArrayList. Nó là strongly typed và nó hỗ trợ độ dài biến đổi của các item.
95. Giải thích Short-Circuit Evaluation trong C#?
Short-circuit evaluation là một phương pháp phức tạp để đánh giá các toán tử logic AND và OR. Trong phương pháp này, toàn bộ biểu thức có thể được đánh giá là true hoặc false mà không cần đánh giá tất cả các biểu thức con. Thông thường Short-circuit chỉ đánh giá phía bên phải nếu phía bên trái chưa xác định kết quả.
if(myObj != null && myObj.SomeString != null)
Mặc dù C# (và một số ngôn ngữ .NET khác) hoạt động theo cách này, nhưng nó là một thuộc tính của ngôn ngữ, không phải của CLR.
96. Sự khác biệt giữa StackOverflowError và OutOfMemoryError là gì?
- OutOfMemoryError liên quan đến Heap. Cần tránh: Đảm bảo có sẵn các đối tượng không cần thiết cho Garbage Collector.
- StackOverflowError liên quan đến stack. Cần tránh: Đảm bảo rằng các cuộc gọi phương thức có kết thúc (không phải trong một vòng lặp vô hạn).
97. Cấu trúc dữ liệu của bạn nên triển khai interface nào để làm cho phương thức Where hoạt động?
Việc triển khai IEnumerable interface giúp có khả năng sử dụng foreach và where.
98. Công dụng của chỉ thị tiền xử lý có điều kiện (conditional preprocessor directive) trong C# là gì?
Bạn có thể sử dụng chỉ thị #if để tạo một chỉ thị có điều kiện. Các chỉ thị có điều kiện rất hữu ích để kiểm tra một ký hiệu hoặc các ký hiệu để kiểm tra xem chúng có là true hay không. Nếu chúng là true, trình biên dịch sẽ thực hiện tất cả mã giữa #if và chỉ thị tiếp theo.
#if DEBUG
Console.WriteLine("Debug version");
#endif
99. Operator overloading (nạp chồng toán tử) có được hỗ trợ trong C# không?
Kiểu do người dùng xác định có thể nạp chồng toán tử được xác định trước trong C#. Có nghĩa là, một kiểu có thể cung cấp triển khai tùy chỉnh của một toán tử trong trường hợp một hoặc cả hai toán hạng thuộc kiểu đó.
public static Box operator+ (Box b, Box c) {
Box box = new Box();
box.length = b.length + c.length;
box.breadth = b.breadth + c.breadth;
box.height = b.height + c.height;
return box;
}
Hàm trên thực hiện toán tử cộng (+) cho một lớp Box do người dùng định nghĩa. Nó thêm các thuộc tính của hai đối tượng Box và trả về đối tượng Box kết quả.
100. Điều gì xảy ra khi chúng ta Box hoặc Unbox các kiểu Nullable?
- Khi một kiểu nullable được box (đóng gói), Common Language Runtime (CLR) sẽ tự động chọn giá trị cơ bản của đối tượng Nullable, không phải chính đối tượng Nullable. Đó là, nếu thuộc tính HasValue là true, nội dung của thuộc tính Value được box. Nếu thuộc tính HasValue là false, null được box.
- Khi giá trị cơ bản của kiểu nullable được unbox (tiến trình ngược lại của box), CLR sẽ tạo ra một cấu trúc Nullable mới được khởi tạo theo giá trị cơ bản đó.
101. Tại sao lớp abstract không thể được sealed?
sealed là một access modifier nếu được áp dụng cho một lớp sẽ làm cho nó không thể kế thừa và nếu áp dụng cho các phương thức hoặc thuộc tính virtual sẽ làm cho chúng trở thành không thể ghi đè (non-overridable). Lớp abstract có ý nghĩa khi bạn muốn tất cả các lớp kế thừa thực hiện cùng một phần logic. Bởi vì một lớp sealed không thể được kế thừa, nó không thể được sử dụng làm lớp cơ sở và do đó, một lớp abstract không thể sử dụng sealed.
Điều quan trọng cần đề cập là các struct hoàn toàn là sealed.
102. Hãy thực hiện phương thức Where trong C# và giải thích đoạn mã đó?
public static IEnumerable<T> Where<T>(this IEnumerable<T> items, Predicate< T> prodicate)
{
foreach(var item in items)
{
if (predicate(item))
{
// for lazy/deffer execution plus avoid temp collection defined
yield return item;
}
}
}
Từ khóa yield thực sự làm khá nhiều việc ở đây. Nó tạo ra một bộ máy trạng thái “dưới vỏ bọc” ghi nhớ vị trí của bạn trong mỗi chu kỳ bổ sung của chức năng và chọn từ đó.
Hàm trả về một đối tượng thực thi IEnumerable<T> interface. Nếu một hàm đang gọi bắt đầu foreach-in đối tượng này, thì hàm sẽ được gọi lại cho đến khi nó “yield” kết quả dựa trên một số predicate. Đây là cú pháp được giới thiệu trong C# 2.0. Trong các phiên bản trước, bạn phải tạo các đối tượng IEnumerable và IEnumerator của riêng mình để thực hiện những công việc như thế này.
103. Mảng jagged trong C# là gì và khi nào thì nên sử dụng mảng jagged thay vì mảng nhiều chiều?
Mảng jagged là một mảng của mảng, vì vậy một int[][] là một mảng của int[], mỗi mảng có thể có độ dài khác nhau và chiếm khối riêng của chúng trong bộ nhớ. Mảng nhiều chiều (int[,]) là một khối bộ nhớ duy nhất (về cơ bản là một ma trận). Cơ bản thì mảng jagged là các mảng “lồng vào nhau” và không cần có kích thước đồng nhất.
Chúng ta có :
int[][] jaggedArray = new int[5][];
jaggedArray[0] = new[] {1, 2, 3}; // 3 item array
jaggedArray[1] = new int[10]; // 10 item array
// etc.
Đó là một tập hợp các mảng liên quan
Mặt khác, mảng nhiều chiều là một nhóm gắn kết hơn, chẳng hạn như hộp, bảng, khối lập phương, v.v., trong đó chúng đồng nhất về độ dài. Điều đó có nghĩa là:
int i = array[1,10];
int j = array[2,10]; // 10 will be available at 2 if available at 1
Ngoài ra, bạn không thể tạo một MyClass[10][20] vì mỗi mảng con phải được khởi tạo riêng biệt, vì chúng là các đối tượng riêng biệt:
MyClass[][] abc = new MyClass[10][];
for (int i=0; i<abc.Length; i++) {
abc[i] = new MyClass[20];
}
Một MyClass[10,20] thì ok, vì nó đang khởi tạo một đối tượng duy nhất dưới dạng ma trận với 10 hàng và 20 cột.
104. Giải thích Weak Reference trong C# là gì?
Garbage Collector (bộ thu gom rác) không thể thu thập một đối tượng đang được ứng dụng sử dụng trong khi mã của ứng dụng có thể tiếp cận đối tượng đó. Ứng dụng đó được cho là có một Strong reference (tham chiếu mạnh) đến đối tượng.
Weak reference (tham chiếu yếu) cho phép Garbage Collector thu thập đối tượng trong khi vẫn cho phép ứng dụng truy cập đối tượng đó. Weak reference chỉ có giá trị trong khoảng thời gian không xác định cho đến khi đối tượng được thu thập khi không có Strong reference nào tồn tại.
Các weak reference rất hữu ích cho các đối tượng sử dụng nhiều bộ nhớ, nhưng dễ dàng được tạo lại bằng cách thu gom rác.
105. Multicast Delegate trong C # là gì?
Delegate chỉ có thể gọi một tham chiếu phương thức đã được đóng gói vào delegate. Một số delegate có thể giữ và gọi nhiều phương thức. Delegate như vậy được gọi là Multicast Delegate. Multicast Delegate (còn được gọi là Combinable Delegate) phải đáp ứng các điều kiện sau:
- Kiểu trả về của Multicast Delegate phải là void. Không có tham số nào của Multicast Delegate có thể được khai báo dưới dạng tham số đầu ra bằng cách sử dụng từ khóa out.
- Instance của Multicast Delegate được tạo bằng cách kết hợp hai delegate, danh sách lệnh gọi được hình thành bằng cách nối danh sách lệnh gọi của hai toán hạng của thao tác cộng (+). Các delegate được gọi theo thứ tự được thêm vào.
Trên thực tế, tất cả các delegate trong C# là Multicast Delegate, ngay cả khi chúng chỉ có một phương thức duy nhất. Ngay cả các hàm anonymous và lambdas cũng là Multicast Delegate mặc dù theo định nghĩa, chúng chỉ có một mục tiêu duy nhất.
public partial class MainPage : PhoneApplicationPage
{
public delegate void MyDelegate(int a, int b);
// Constructor
public MainPage()
{
InitializeComponent();
// Multicast delegate
MyDelegate myDel = new MyDelegate(AddNumbers);
myDel += new MyDelegate(MultiplyNumbers);
myDel(10, 20);
}
public void AddNumbers(int x, int y)
{
int sum = x + y;
MessageBox.Show(sum.ToString());
}
public void MultiplyNumbers(int x, int y)
{
int mul = x * y;
MessageBox.Show(mul.ToString());
}
}
106. Khái niệm Deep Copy và Shallow Copy trong C# là gì?
- Shallow Copy: tạo một đối tượng mới và sau đó sao chép các thuộc tính kiểu giá trị của đối tượng được sao chép sang đối tượng mới. Nhưng khi thuộc tính là kiểu tham chiếu, thì chỉ có tham chiếu là được sao chép chứ không phải bản thân đối tượng được thuộc tính đó tham chiếu đến. Do đó thuộc tính kiểu tham chiếu của bản gốc và bản sao đều tham chiếu đến cùng một đối tượng. Khái niệm này sẽ rõ ràng hơn khi bạn nhìn thấy sơ đồ của Shallow Copy.
- Deep Copy: nó là một quá trình tạo một đối tượng mới và sau đó sao chép các thuộc tính của đối tượng được sao chép sang đối tượng mới để tạo một bản sao hoàn chỉnh của các kiểu tham chiếu bên trong, vì điều này, chúng ta cần cấu hình đối tượng được trả về bằng phương thức MemberwiseClone(). Nếu thuộc tính được sao chép là một kiểu giá trị, thì một bản sao từng bit của thuộc tính đó sẽ được thực hiện. Nếu thuộc tính được sao chép là một kiểu tham chiếu, thì đối tượng được thuộc tính đó tham chiếu đến sẽ được sao chép.
107. Preprocessor directives trong C# là gì?
Các preprocessor directives cung cấp hướng dẫn cho trình biên dịch để xử lý thông tin trước khi quá trình biên dịch thực sự bắt đầu. Nói chung, các ký hiệu biên dịch tùy chọn / có điều kiện sẽ được cung cấp bởi build script.
#define DEBUG
// ...
#if DEBUG
Console.WriteLine("Debug version");
#endif
Tôi thực sự khuyên bạn nên sử dụng Conditional Attribute thay vì các câu lệnh #if nội tuyến.
[Conditional("DEBUG")]
private void DeleteTempProcessFiles()
{
}
Điều này không chỉ gọn gàng hơn và dễ đọc hơn vì bạn không có #if, #else trong mã của mình, mà còn ít bị lỗi hơn trong quá trình chỉnh sửa mã bình thường và cũng như các lỗi luồng logic.
108. Sự khác biệt giữa các interface: IQueryable, ICollection, IList và IDictionary là gì?
Tất cả các interface này đều kế thừa từ IEnumerable. Về cơ bản, interface đó cho phép bạn sử dụng lớp trong một câu lệnh foreach (trong C#).
- ICollection là interface cơ bản nhất trong số các interface đã liệt kê ở trên. ICollection là một interface có hỗ trợ Count.
- IList là tất cả mọi thứ của ICollection, nhưng nó cũng hỗ trợ thêm và xóa các mục, truy xuất các mục bằng chỉ mục, v.v. Đây là interface được sử dụng phổ biến nhất cho “danh sách các đối tượng”.
- IQueryable là một interface có hỗ trợ LINQ. Bạn luôn có thể tạo IQueryable từ IList và sử dụng LINQ to Objects, nhưng bạn cũng thấy IQueryable được sử dụng cho deferred execution của các câu lệnh SQL trong LINQ to SQL và LINQ to Entities.
- IDictionary là một interface khác theo nghĩa nó là một ánh xạ các khóa duy nhất đến các giá trị. Nó cũng là enumerable ở chỗ bạn có thể liệt kê các cặp khóa / giá trị, và nó phục vụ một mục đích khác với các interface liệt kê trên.
109. Liệt kê một số cách khác nhau để kiểm tra equality (==) trong .NET?
- Phương thức ReferenceEquals(): kiểm tra xem hai biến kiểu tham chiếu (class, không phải struct) có được tham chiếu đến cùng một địa chỉ bộ nhớ hay không.
- Phương thức virtual Equals() (System.Object): kiểm tra xem hai đối tượng có tương đương nhau không.
- Phương thức static Equals(): được sử dụng để xử lý các vấn đề khi có giá trị null trong kiểm tra đó.
- Phương thức Equals từ IEquatable interface.
- Toán tử ==: thường có nghĩa giống như ReferenceEquals, nó kiểm tra xem hai biến có trỏ đến cùng một địa chỉ bộ nhớ hay không. Điểm then chốt là toán tử này có thể được ghi đè (override) để thực hiện các loại kiểm tra khác. Ví dụ, trong string, nó kiểm tra xem hai instance khác nhau có tương đương hay không.
110. Bạn có thể giải thích sự khác biệt giữa các phương thức destructor, dispose và finalize?
Theo thuật ngữ C#, destructor và finalize về cơ bản là các khái niệm có thể hoán đổi cho nhau và nên được sử dụng để giải phóng các tài nguyên không được quản lý, ví dụ như các xử lý bên ngoài. Rất hiếm khi bạn cần phải viết finalize.
Garbage Collector là không xác định (non-deterministic), vì vậy phương thức Dispose() (thông qua IDisposable) có thể hỗ trợ xác định dọn dẹp. Điều này không liên quan đến việc thu gom rác và cho phép người gọi giải phóng bất kỳ tài nguyên nào sớm hơn. Nó cũng thích hợp để sử dụng với các tài nguyên được quản lý (ngoài tài nguyên không được quản lý), ví dụ: nếu bạn có một kiểu đóng gói (giả sử) một kết nối cơ sở dữ liệu, bạn cũng có thể muốn loại bỏ kiểu này để giải phóng kết nối.
111. Lợi ích của Deferred Execution trong LINQ là gì?
Trong LINQ, các truy vấn có hai hành vi thực thi khác nhau: immediate (ngay lập tức) và deferred (trì hoãn).
Deferred execution có nghĩa là đánh giá một biểu thức bị trì hoãn (delay) cho đến khi giá trị thực của nó thực sự được yêu cầu. Nó cải thiện đáng kể hiệu suất bằng cách tránh thực thi không cần thiết.
var results = collection.Select(item => item.Foo).Where(foo => foo < 3).ToList();
Với deferred execution, đoạn mã trên lặp qua collection của bạn một lần và mỗi khi một mục được yêu cầu trong quá trình lặp, chúng thực hiện thao tác map, filter, sau đó sử dụng kết quả để xây dựng nên danh sách.
Nếu bạn muốn LINQ thực thi đầy đủ mỗi lần, mỗi thao tác (Select / Where) sẽ phải lặp qua toàn bộ collection. Điều này sẽ làm cho chuỗi hoạt động rất kém hiệu quả.
112. Từ khóa volatile được sử dụng để làm gì?
Trong C# volatile nói với trình biên dịch rằng giá trị của một biến không bao giờ được lưu trong bộ đệm (cache) vì giá trị của nó có thể thay đổi bên ngoài phạm vi của chính chương trình (chẳng hạn như hệ điều hành, phần cứng hoặc một thread thực thi đồng thời). Sau đó, trình biên dịch sẽ tránh mọi tối ưu hóa có thể dẫn đến sự cố nếu biến thay đổi “ngoài tầm kiểm soát của nó”.
Hay nói một cách đơn giản hơn:
Đôi khi, trình biên dịch sẽ tối ưu hóa một trường (field) và sử dụng một thanh ghi (register) để lưu trữ nó. Nếu thread 1 thực hiện ghi vào trường đó và thread khác truy cập nó, thread thứ 2 đó sẽ nhận được dữ liệu cũ vì giá trị thay đổi sẽ được lưu trữ trong một thanh ghi (chứ không phải bộ nhớ).
Bạn có thể nghĩ về từ khóa volatile như nói với trình biên dịch “Tôi muốn bạn lưu trữ giá trị này trong bộ nhớ”. Điều này đảm bảo rằng thread thứ 2 truy xuất giá trị mới nhất.
113. Phương thức MemberwiseClone() dùng để làm gì?
Phương thức MemberwiseClone() tạo một shadow copy bằng cách tạo một đối tượng mới, sau đó sao chép các thuộc tính non-static của đối tượng hiện tại sang đối tượng mới.
- Nếu thuộc tính là một kiểu giá trị, một bản sao từng bit của thuộc tính đó sẽ được thực hiện.
- Nếu thuộc tính là kiểu tham chiếu, thì tham chiếu được sao chép nhưng đối tượng được tham chiếu thì không; do đó, đối tượng gốc và bản sao của nó tham chiếu đến cùng một đối tượng.
public class Person
{
public int Age;
public string Name;
public IdInfo IdInfo;
public Person ShallowCopy()
{
return (Person)this.MemberwiseClone();
}
public Person DeepCopy()
{
Person other = (Person)this.MemberwiseClone();
other.IdInfo = new IdInfo(IdInfo.IdNumber);
other.Name = String.Copy(Name);
return other;
}
}
114. Bạn có thể thêm các phương thức mở rộng vào một lớp static đã có không?
Không. Các phương thức mở rộng (extension method) yêu cầu một biến instance (giá trị) cho một đối tượng. Tuy nhiên, bạn có thể viết một lớp wrapper static.
public static string SomeStringExtension(this string s)
{
//whatever..
}
// When you then call it
myString.SomeStringExtension();
// the compiler just turns it into:
ExtensionClass.SomeStringExtension(myString);
115. Công dụng của hàm tạo static là gì?
Hàm tạo static (static constructor) hữu ích khi khởi tạo bất kỳ trường static nào được liên kết với một kiểu (hoặc bất kỳ hoạt động nào khác trên mỗi kiểu) – đặc biệt hữu ích cho các trường chỉ để đọc các dữ liệu cấu hình, v.v.
Nó được chạy tự động trong runtime lần đầu tiên khi cần thiết (các quy tắc chính xác rất phức tạp (xem “beforefieldinit”) và được thay đổi một cách tinh vi giữa CLR2 và CLR4). Nếu bạn không lạm dụng reflection, nó được đảm bảo chạy nhiều nhất là một lần (ngay cả khi hai thread đến cùng một lúc).
Bạn không thể nạp chồng nó.
116. Sự khác biệt giữa Lambda và Delegate là gì?
Chúng thực sự là hai thứ rất khác nhau.
- Delegate thực sự là tên của một biến chứa tham chiếu đến một phương thức hoặc lambda, và lambda là một phương thức không có tên cố định.
delegate Int32 BinaryIntOp(Int32 x, Int32 y);
- Lambda rất giống các phương thức khác, ngoại trừ một vài khác biệt nhỏ:
- Một phương thức bình thường được định nghĩa trong một “statement” và gắn với một tên cố định, trong khi một lambda được định nghĩa trong một “biểu thức” và không có tên cố định.
- Lambda có thể được sử dụng với cây biểu thức .NET, trong khi các phương thức thì không thể.
Một lambda có thể được định nghĩa như thế này:
BinaryIntOp sumOfSquares = (a, b) => a*a + b*b;
117. Bạn đã định nghĩa một hàm hủy trong một lớp mà bạn đang phát triển bằng cách sử dụng ngôn ngữ lập trình C#, nhưng hàm hủy không bao giờ được thực thi. Tại sao hàm hủy đã không thực thi?
Môi trường runtime tự động gọi hàm hủy (destructor) của một lớp để giải phóng tài nguyên bị chiếm bởi các biến và phương thức của một đối tượng. Tuy nhiên, trong C#, các lập trình viên không thể kiểm soát thời gian gọi các hàm hủy, vì Garbage Collector chỉ chịu trách nhiệm giải phóng các tài nguyên được sử dụng bởi một đối tượng. Garbage Collector tự động lấy thông tin về các đối tượng không được tham chiếu từ môi trường runtime của .NET và sau đó gọi phương thức Finalize().
Mặc dù, việc ép Garbage Collector thực hiện thu gom rác và truy xuất tất cả bộ nhớ không thể truy cập được là không thích hợp, lập trình viên có thể sử dụng phương thức Collect() của lớp Garbage Collector để buộc thực thi Garbage Collector.
118. Sự khác biệt giữa System.Array.CopyTo() và System.Array.Clone() là gì?
Array.Clone() tạo một bản sao của mảng ban đầu. Nó trả về một mảng có độ dài bằng với mảng ban đầu. Phương thức Array.Clone() không yêu cầu mảng đích tồn tại sẵn vì nó tự tạo ra một mảng mới.
Array.CopyTo() sao chép các phần tử từ mảng ban đầu sang mảng đích bắt đầu từ chỉ số được chỉ định. Lưu ý rằng, việc này sẽ thêm các phần tử được chỉ định sao chép từ mảng ban đầu vào mảng đích.
Array.CopyTo() yêu cầu một mảng đích đã tồn tại trước đó và nó phải có khả năng giữ tất cả các phần tử được chỉ định sao chép từ mảng ban đầu.
Array.Clone() và Array.CopyTo() đều thực hiện một shadow copy.
119. Sự khác biệt giữa IEnumerable và IQueryable là gì?
- IEnumerable<> interface chỉ ra rằng một cái gì đó có thể được liệt kê qua toàn bộ – nói cách khác, bạn có thể thực hiện một vòng lặp foreach trên đó.
- IQueryable<> interface chỉ ra rằng thứ gì đó có một số loại nhà cung cấp truy vấn hỗ trợ có khả năng xem các biểu thức được cấp cho nó, và dịch chúng thành một số loại truy vấn.
IQueryable có hai thuộc tính cụ thể mà IEnumerable không có — một thuộc tính trỏ đến trình cung cấp truy vấn (ví dụ: trình cung cấp LINQ to SQL) và một thuộc tính khác trỏ đến biểu thức truy vấn đại diện cho đối tượng IQueryable dưới dạng cây cú pháp trừu tượng có thể duyệt qua runtime mà nhà cung cấp truy vấn đó có thể hiểu được (đối với hầu hết các trường hợp, bạn không thể đưa một biểu thức LINQ to SQL tới nhà cung cấp LINQ to Entities mà không có exception nào được thảy ra).