Bộ câu hỏi phỏng vấn C#/.Net phần 8

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

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,... 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.

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#, destructorfinalize 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.

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.

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.

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,...

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ó.

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ả.

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;

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#).

  • ICollectioninterface 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.

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

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.

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.

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.

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.

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? (master)


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.

Avatar Techmely Team
VIẾT BỞI

Techmely Team