Khi nào sử dụng con trỏ trong C # /. NET?


76

Tôi biết C # cung cấp cho lập trình viên khả năng truy cập, sử dụng con trỏ trong bối cảnh không an toàn. Nhưng điều này là cần thiết khi nào?

Trong những trường hợp nào, việc sử dụng con trỏ trở nên không thể tránh khỏi?

Nó chỉ vì lý do hiệu suất?

Ngoài ra, tại sao C # lại bộc lộ chức năng này thông qua một ngữ cảnh không an toàn và loại bỏ tất cả các lợi thế được quản lý khỏi nó? Về mặt lý thuyết, liệu có thể có con trỏ sử dụng mà không làm mất bất kỳ lợi thế nào của môi trường được quản lý?


Cảm ơn Richard, bạn chỉ cần cố gắng tìm hiểu thêm bằng cách đặt (thêm) câu hỏi: O
Joan Venge

3
Câu hỏi này có lẽ sẽ quan tâm đến bạn: stackoverflow.com/questions/584134/...
Hans Olsson

Câu trả lời:


91

Khi nào điều này là cần thiết? Trong những trường hợp nào thì việc sử dụng con trỏ trở nên không thể tránh khỏi?

Khi chi phí ròng của một giải pháp an toàn được quản lý là không thể chấp nhận được nhưng chi phí ròng của một giải pháp không an toàn lại có thể chấp nhận được. Bạn có thể xác định chi phí ròng hoặc lợi ích ròng bằng cách lấy tổng chi phí trừ đi tổng lợi ích. Những lợi ích của một giải pháp không an toàn là những thứ như "không lãng phí thời gian cho việc kiểm tra thời gian chạy không cần thiết để đảm bảo tính đúng đắn"; chi phí là (1) phải viết mã an toàn ngay cả khi hệ thống an toàn được quản lý bị tắt, và (2) phải đối phó với khả năng làm cho bộ thu gom rác kém hiệu quả hơn, vì nó không thể di chuyển xung quanh bộ nhớ có con trỏ không được quản lý vào nó.

Hoặc, nếu bạn là người viết lớp điều phối.

Nó chỉ vì lý do hiệu suất?

Có vẻ như việc sử dụng con trỏ trong ngôn ngữ được quản lý vì những lý do khác ngoài hiệu suất.

Bạn có thể sử dụng các phương thức trong lớp Marshal để xử lý việc tương tác với mã không được quản lý trong đại đa số các trường hợp. (Có thể có một số trường hợp khó hoặc không thể sử dụng thiết bị điều chỉnh để giải quyết vấn đề liên động, nhưng tôi không biết về bất kỳ trường hợp nào.)

Tất nhiên, như tôi đã nói, nếu bạn là người viết lớp Marshal thì rõ ràng bạn không thể sử dụng lớp Marshal để giải quyết vấn đề của mình. Trong trường hợp đó, bạn cần triển khai nó bằng cách sử dụng con trỏ.

Tại sao C # lại để lộ chức năng này thông qua một bối cảnh không an toàn và loại bỏ tất cả các lợi thế được quản lý khỏi nó?

Những lợi thế được quản lý đó đi kèm với chi phí hiệu suất. Ví dụ: mỗi khi bạn yêu cầu mảng cho phần tử thứ mười của nó, thời gian chạy cần thực hiện kiểm tra xem có phần tử thứ mười hay không và đưa ra một ngoại lệ nếu không có. Với con trỏ rằng chi phí thời gian chạy được loại bỏ.

Cái giá phải trả cho nhà phát triển tương ứng là nếu bạn làm sai thì bạn phải đối phó với lỗi hỏng bộ nhớ định dạng đĩa cứng của bạn và làm hỏng quy trình của bạn một giờ sau đó thay vì xử lý một ngoại lệ sạch đẹp tại điểm xảy ra lỗi.

Có thể sử dụng con trỏ mà không làm mất bất kỳ lợi thế nào của môi trường được quản lý, về mặt lý thuyết?

Theo "ưu điểm", tôi giả sử bạn muốn nói đến các lợi thế như thu gom rác, an toàn kiểu và tính toàn vẹn tham chiếu. Vì vậy, câu hỏi của bạn về cơ bản là "về lý thuyết có thể tắt hệ thống an toàn nhưng vẫn nhận được những lợi ích của hệ thống an toàn đang được bật?" Không, rõ ràng là không. Nếu bạn tắt hệ thống an toàn đó vì bạn không thích nó đắt tiền như thế nào thì bạn sẽ không nhận được những lợi ích của nó!


Cảm ơn Eric đã trả lời. Bạn có thể vui lòng cho tôi biết "tính toàn vẹn tham chiếu" có nghĩa là gì? Nó có phải là việc sử dụng các tham chiếu thay vì con trỏ?
Joan Venge

9
@Joan: Rằng mọi tham chiếu thực sự đề cập đến một cái gì đó hợp lệ hoặc vô hiệu . Con trỏ không có thuộc tính đó; một con trỏ có thể đề cập đến bộ nhớ không tốt chút nào. Nhưng các tham chiếu được quản lý có thuộc tính đó; nếu bạn có một tham chiếu đến một chuỗi, thì thứ đó luôn là null hoặc một chuỗi hợp lệ; bạn được đảm bảo không ở trong trường hợp bạn có tham chiếu không rỗng đến một thứ gì đó không phải là một chuỗi hợp lệ.
Eric Lippert

Cảm ơn Eric, tôi đã hiểu nó bây giờ.
Joan Venge

2
@masoudkeshavarz: Không. Với con trỏ được quản lý, không thể giả mạo một con trỏ vào bộ nhớ tùy ý. Với các con trỏ không được quản lý trong mã không an toàn, giả sử chúng được gọi là "không được quản lý" và "không an toàn" vì một lý do . Bạn có thể làm bất cứ điều gì bạn thích với một con trỏ không được quản lý trong mã không an toàn, bao gồm cả việc làm hỏng cấu trúc dữ liệu thời gian chạy .NET.
Eric Lippert

1
thánh địa ngục, đã tìm kiếm một câu trả lời rõ ràng không có bs trong một giờ và điều này thật tuyệt vời. cảm ơn bạn!
krivar

17

Con trỏ là một mâu thuẫn cố hữu đối với môi trường được quản lý, thu gom rác thải.
Một khi bạn bắt đầu nhầm lẫn với các con trỏ thô, GC không có manh mối gì đang xảy ra.

Cụ thể, nó không thể cho biết liệu các đối tượng có thể truy cập được hay không, vì nó không biết con trỏ của bạn đang ở đâu.
Nó cũng không thể di chuyển các đối tượng xung quanh trong bộ nhớ, vì điều đó sẽ phá vỡ các con trỏ của bạn.

Tất cả điều này sẽ được giải quyết bởi các con trỏ được GC theo dõi; đó là những gì tài liệu tham khảo.

Bạn chỉ nên sử dụng con trỏ trong các kịch bản tương tác nâng cao lộn xộn hoặc để tối ưu hóa rất phức tạp.
Nếu bạn phải hỏi, có lẽ bạn không nên.


7
+1 cho Nếu bạn phải hỏi, có lẽ bạn không nên . Lời khuyên tuyệt vời :-)
Darin Dimitrov

1
Kết luận của bạn là đúng, nhưng phần lớn lời giải thích của bạn là sai. Con trỏ và tham chiếu không khác gì so với quan điểm của trình thu gom rác. Điều phá vỡ GC là khi một con trỏ hoặc tham chiếu được lưu trữ trong vùng bộ nhớ chưa định kiểu, vì GC không còn biết liệu đó chỉ là giá trị số hay địa chỉ của đối tượng được quản lý.
Ben Voigt

1
@SLaks: Tôi không nói rằng các tham chiếu và con trỏ không khác nhau, tôi đã nói rằng chúng không khác với quan điểm của người thu gom rác . GC không thể quan tâm hơn đến việc bạn đã lấy địa chỉ của một phần tử mảng hay bắt đầu bằng một con trỏ tới một phần tử khác và thực hiện số học để tìm phần tử bạn đang trỏ vào lúc này.
Ben Voigt

1
@SLaks: Ngay cả trong C và C ++ nguyên bản, số học con trỏ chỉ được phép trong giới hạn của một đối tượng / cấp phát duy nhất (ví dụ: một mảng). Trình thu gom rác vẫn di chuyển toàn bộ các đối tượng lại với nhau, các con trỏ sẽ không bị hỏng.
Ben Voigt

3
@SLaks: Đáng kể. BTW, con trỏ được theo dõi GC giả định của bạn không tồn tại trong các ngôn ngữ .NET khác (mặc dù có một số hạn chế - nó chỉ có thể là một biến tự động) và nó hỗ trợ số học:interior_ptr
Ben Voigt

5

GC có thể di chuyển các tham chiếu xung quanh; sử dụng không an toàn sẽ giữ một đối tượng nằm ngoài tầm kiểm soát của GC và tránh điều này. "Cố định" ghim một đối tượng, nhưng cho phép GC quản lý bộ nhớ.

Theo định nghĩa, nếu bạn có một con trỏ đến địa chỉ của một đối tượng và GC di chuyển nó, con trỏ của bạn không còn hợp lệ.

Về lý do tại sao bạn cần con trỏ: Lý do chính là làm việc với các tệp DLL không được quản lý, ví dụ như những tệp được viết bằng C ++

Cũng lưu ý, khi bạn ghim các biến và sử dụng con trỏ, bạn dễ bị phân mảnh đống.


Biên tập

Bạn đã đề cập đến vấn đề cốt lõi của mã được quản lý và không được quản lý ... làm thế nào để bộ nhớ được giải phóng?

Bạn có thể trộn mã cho hiệu suất như bạn mô tả, bạn chỉ không thể vượt qua ranh giới được quản lý / không được quản lý với con trỏ (tức là bạn không thể sử dụng con trỏ bên ngoài ngữ cảnh 'không an toàn').

Về cách chúng được làm sạch ... Bạn phải quản lý bộ nhớ của chính mình; các đối tượng mà con trỏ của bạn trỏ đến đã được tạo / cấp phát (thường trong C ++ DLL) bằng cách sử dụng (hy vọng) CoTaskMemAlloc()và bạn phải giải phóng bộ nhớ đó theo cách tương tự, gọi CoTaskMemFree(), nếu không bạn sẽ bị rò rỉ bộ nhớ. Lưu ý rằng chỉ có bộ nhớ được cấp phát với CoTaskMemAlloc()có thể được giải phóng với CoTaskMemFree().

Giải pháp thay thế khác là hiển thị một phương thức từ dll C ++ gốc của bạn lấy một con trỏ và giải phóng nó ... điều này cho phép DLL quyết định cách giải phóng bộ nhớ, hoạt động tốt nhất nếu nó sử dụng một số phương pháp khác để cấp phát bộ nhớ. Hầu hết các hình nền gốc mà bạn làm việc đều là các hình nền của bên thứ ba mà bạn không thể sửa đổi và chúng thường không có (mà tôi đã thấy) các chức năng như vậy để gọi.

Một ví dụ về giải phóng bộ nhớ, được lấy từ đây :

string[] array = new string[2];
array[0] = "hello";
array[1] = "world";
IntPtr ptr = test(array);
string result = Marshal.PtrToStringAuto(ptr);
Marshal.FreeCoTaskMem(ptr);
System.Console.WriteLine(result);


Một số tài liệu đọc thêm:

Bộ nhớ deallocate C # được tham chiếu bởi IntPtr Câu trả lời thứ hai giải thích các phương pháp phân bổ / deallocation khác nhau

Làm thế nào để giải phóng IntPtr trong C #? Củng cố nhu cầu phân bổ theo cùng một cách bộ nhớ đã được cấp phát

http://msdn.microsoft.com/en-us/library/aa366533%28VS.85%29.aspx Tài liệu MSDN chính thức về các cách khác nhau để cấp phát và phân bổ bộ nhớ.

Tóm lại ... bạn cần biết bộ nhớ được cấp phát như thế nào để giải phóng nó.


Chỉnh sửa Nếu tôi hiểu chính xác câu hỏi của bạn, câu trả lời ngắn gọn là có, bạn có thể chuyển dữ liệu cho các con trỏ không được quản lý, làm việc với nó trong bối cảnh không an toàn và có sẵn dữ liệu khi bạn thoát khỏi ngữ cảnh không an toàn.

Điều quan trọng là bạn phải ghim đối tượng được quản lý mà bạn đang tham chiếu với một fixedkhối. Điều này ngăn không cho bộ nhớ bạn đang tham chiếu bị GC di chuyển khi ở trong unsafekhối. Có một số vấn đề phức tạp ở đây, chẳng hạn như bạn không thể gán lại con trỏ được khởi tạo trong một khối cố định ... bạn nên đọc các câu lệnh không an toàn và cố định nếu bạn thực sự muốn quản lý mã của riêng mình.

Tất cả những gì đã nói, lợi ích của việc quản lý các đối tượng của riêng bạn và sử dụng con trỏ theo cách bạn mô tả có thể không giúp bạn tăng hiệu suất nhiều như bạn nghĩ. Lý do tại sao không:

  1. C # rất tối ưu hóa và rất nhanh
  2. Mã con trỏ của bạn vẫn được tạo dưới dạng IL, mã này phải được ghép nối (tại thời điểm này, các tính năng tối ưu hóa tiếp theo sẽ hoạt động)
  3. Bạn không tắt Trình thu gom rác ... bạn chỉ giữ các đối tượng bạn đang làm việc ngoài tầm nhìn của GC. Vì vậy, cứ sau 100 mili giây, GC vẫn ngắt mã của bạn và thực thi các chức năng của nó cho tất cả các biến khác trong mã được quản lý của bạn.

HTH,
James


Cảm ơn, nhưng khi bạn sử dụng con trỏ, chúng sẽ được "làm sạch" như thế nào sau khi bạn làm xong? Có thể sử dụng chúng trong các tình huống phê bình hoàn hảo và sau đó chuyển về mã được quản lý không?
Joan Venge

Cảm ơn James cho thông tin bổ sung.
Joan Venge

2
@Joan: Chắc chắn rồi. Nhưng bạn có trách nhiệm đảm bảo rằng mọi thứ đều được dọn dẹp, không có con trỏ lạc hướng nào đến bộ nhớ có thể di chuyển nằm xung quanh, v.v. Nếu bạn muốn những lợi ích của việc tắt hệ thống an toàn thì bạn phải chịu những chi phí để thực hiện những gì mà hệ thống an toàn thường làm cho bạn.
Eric Lippert

Cảm ơn Eric, điều đó có ý nghĩa. Nhưng trong trường hợp tối ưu hóa hiệu suất thông qua con trỏ, một người vẫn sẽ đưa dữ liệu trở lại thế giới được quản lý sau khi anh ta / cô ta hoàn thành, phải không? Như dữ liệu được quản lý -> dữ liệu không được quản lý -> một số thao tác nhanh trên dữ liệu này -> tạo dữ liệu được quản lý từ dữ liệu không được quản lý này -> xóa sạch bộ nhớ không được quản lý -> quay lại thế giới được quản lý?
Joan Venge

1
Lưu ý thêm, bạn có thể thông báo rõ ràng việc thu gom rác về áp lực bộ nhớ từ bộ nhớ không được quản lý bằng cách sử dụng GC.AddMemoryPressureGC.RemoveMemoryPressure. Bạn sẽ vẫn phải tự giải phóng bộ nhớ, nhưng bằng cách này, trình thu gom rác sẽ tính đến bộ nhớ không được quản lý khi đưa ra quyết định lên lịch.
Brian

3

Những lý do phổ biến nhất để sử dụng con trỏ một cách rõ ràng trong C #:

  • thực hiện công việc cấp thấp (như thao tác chuỗi) rất nhạy cảm về hiệu suất,
  • giao tiếp với các API không được quản lý.

Lý do tại sao cú pháp liên kết với con trỏ bị xóa khỏi C # (theo hiểu biết và quan điểm của tôi - Jon Skeet sẽ trả lời tốt hơn B-))) hóa ra nó không cần thiết trong hầu hết các tình huống.

Từ quan điểm thiết kế ngôn ngữ, một khi bạn quản lý bộ nhớ bằng bộ thu gom rác, bạn phải đưa ra những hạn chế nghiêm trọng về những gì được và những gì không thể làm với con trỏ. Ví dụ, việc sử dụng một con trỏ để trỏ vào giữa một đối tượng có thể gây ra các vấn đề nghiêm trọng cho GC. Do đó, khi các hạn chế được áp dụng, bạn chỉ có thể bỏ qua cú pháp bổ sung và kết thúc với các tham chiếu "tự động".

Ngoài ra, cách tiếp cận siêu nhân từ được tìm thấy trong C / C ++ là một nguồn lỗi phổ biến. Đối với hầu hết các tình huống, nơi hiệu suất vi mô không quan trọng chút nào, tốt hơn nên đưa ra các quy tắc chặt chẽ hơn và hạn chế nhà phát triển có lợi cho ít lỗi hơn mà rất khó phát hiện ra. Vì vậy, đối với các ứng dụng kinh doanh thông thường, môi trường được gọi là “được quản lý” như .NET và Java phù hợp hơn so với các ngôn ngữ được cho là hoạt động với cỗ máy thô sơ.


1
Con trỏ không bị xóa khỏi C #. Có lẽ bạn đang nghĩ về Java?
Ben Voigt

Ý tôi không phải là con trỏ đã bị loại bỏ nhưng cú pháp bổ sung đã bị loại bỏ, tức là không phải viết obj->Property, obj.Propertythay vào đó sẽ hoạt động. Sẽ làm rõ câu trả lời của tôi.
Ondrej Tucny


1
Ben nói đúng; chắc chắn bạn phải sử dụng mũi tên (và dấu sao) khi tham chiếu con trỏ trong C #. Đừng nhầm lẫn con trỏ với tham chiếu ; C # hỗ trợ cả hai.
Eric Lippert

@Eric Lippert Heh, vâng. Tuy nhiên, khi nghĩ về một tham chiếu như một tập hợp con của con trỏ, tôi đã chọn từ 'con trỏ' làm biến thể có mục đích chung hơn để giải thích sự phát triển của một tham chiếu — và ngôn ngữ "không có con trỏ" (phần 'an toàn' của nó để đúng) —từ con trỏ cũ đơn giản.
Ondrej Tucny

2

Giả sử bạn muốn giao tiếp giữa 2 ứng dụng bằng IPC (bộ nhớ dùng chung) thì bạn có thể sắp xếp dữ liệu vào bộ nhớ và chuyển con trỏ dữ liệu này cho ứng dụng khác thông qua tin nhắn cửa sổ hoặc một cái gì đó. Khi nhận ứng dụng, bạn có thể lấy lại dữ liệu.

Cũng hữu ích trong trường hợp chuyển dữ liệu từ .NET sang các ứng dụng VB6 cũ, trong đó bạn sẽ điều chỉnh dữ liệu vào bộ nhớ, chuyển con trỏ tới ứng dụng VB6 bằng cách sử dụng win msging, sử dụng VB6 copymemory () để tìm nạp dữ liệu từ không gian bộ nhớ được quản lý sang bộ nhớ không được quản lý của ứng dụng VB6 không gian..

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.