Microsoft có khuyến nghị sử dụng các thuộc tính C # để áp dụng cho phát triển trò chơi không?


26

Tôi nhận được rằng đôi khi bạn cần các thuộc tính, như:

public int[] Transitions { get; set; }

hoặc là:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

Nhưng ấn tượng của tôi là trong quá trình phát triển trò chơi, trừ khi bạn có lý do để làm khác, mọi thứ nên nhanh nhất có thể. Ấn tượng của tôi từ những điều tôi đã đọc là Unity 3D không chắc chắn truy cập nội tuyến thông qua các thuộc tính, vì vậy sử dụng một thuộc tính sẽ dẫn đến một cuộc gọi chức năng bổ sung.

Tôi có đúng không khi liên tục bỏ qua các đề xuất của Visual Studio để không sử dụng các lĩnh vực công cộng? (Lưu ý rằng chúng tôi sử dụng int Foo { get; private set; }khi có lợi thế trong việc hiển thị mục đích sử dụng.)

Tôi biết rằng tối ưu hóa sớm là không tốt, nhưng như tôi đã nói, trong quá trình phát triển trò chơi, bạn không làm điều gì đó chậm chạp khi một nỗ lực tương tự có thể làm cho nó nhanh.


34
Luôn xác minh với một hồ sơ trước khi tin rằng một cái gì đó là "chậm". Tôi được cho biết một cái gì đó là "chậm" và trình hồ sơ nói với tôi rằng nó phải thực hiện 500.000.000 lần để mất nửa giây. Vì nó sẽ không bao giờ thực hiện hơn vài trăm lần trong một khung, tôi quyết định nó không liên quan.
Almo

5
Tôi đã thấy rằng một chút trừu tượng ở đây và ở đó giúp áp dụng tối ưu hóa mức độ thấp rộng rãi hơn. Ví dụ, tôi đã thấy nhiều dự án C đơn giản sử dụng các loại danh sách liên kết khác nhau, tốc độ chậm khủng khiếp so với các mảng liền kề (ví dụ: sao lưu cho ArrayList). Điều này là do C không có khái quát và các lập trình viên C này có thể thấy việc thực hiện lại nhiều lần các danh sách liên kết dễ dàng hơn so với thực hiện tương tự đối với ArrayListsthuật toán thay đổi kích thước. Nếu bạn đưa ra một tối ưu hóa cấp thấp tốt, hãy gói gọn nó!
hegel5000

3
@Almo Bạn có áp dụng logic tương tự cho các hoạt động xảy ra ở 10.000 địa điểm khác nhau không? Bởi vì đó là truy cập thành viên lớp. Về mặt logic, bạn nên nhân chi phí hiệu năng lên 10 nghìn trước khi quyết định rằng lớp tối ưu hóa không thành vấn đề. Và 0,5 ms là một quy tắc tốt hơn khi hiệu suất của trò chơi của bạn sẽ bị hủy hoại, không phải là nửa giây. Quan điểm của bạn vẫn đứng vững, mặc dù tôi không nghĩ hành động đúng là rõ ràng như bạn đưa ra.
piojo

5
Bạn có 2 con ngựa. Đua chúng.
Cột

@piojo Tôi không có. Một số suy nghĩ phải luôn luôn đi vào những điều này. Trong trường hợp của tôi, nó là cho các vòng lặp trong mã UI. Một cá thể UI, vài vòng lặp, vài lần lặp. Đối với hồ sơ, tôi nêu lên nhận xét của bạn. :)
Almo

Câu trả lời:


44

Nhưng ấn tượng của tôi là trong phát triển trò chơi, trừ khi bạn có lý do để làm khác, mọi thứ nên nhanh nhất có thể.

Không cần thiết. Giống như trong phần mềm ứng dụng, có một mã trong trò chơi rất quan trọng về hiệu năng và mã không phải là mã.

Nếu mã được thực thi vài nghìn lần trên mỗi khung, thì việc tối ưu hóa ở mức thấp như vậy có thể có ý nghĩa. (Mặc dù trước tiên bạn có thể muốn kiểm tra trước nếu thực sự cần thiết phải gọi nó thường xuyên. Mã nhanh nhất là mã bạn không chạy)

Nếu mã được thực thi một vài giây một lần, thì khả năng đọc và bảo trì quan trọng hơn nhiều so với hiệu suất.

Tôi đã đọc là Unity 3D không chắc chắn truy cập nội tuyến thông qua các thuộc tính, vì vậy sử dụng một thuộc tính sẽ dẫn đến một cuộc gọi chức năng bổ sung.

Với các thuộc tính tầm thường trong phong cách của int Foo { get; private set; }bạn, có thể khá chắc chắn rằng trình biên dịch sẽ tối ưu hóa trình bao bọc thuộc tính đi. Nhưng:

  • nếu bạn thực sự có một triển khai, thì bạn không thể chắc chắn được nữa. Việc thực hiện càng phức tạp, trình biên dịch sẽ càng ít có khả năng nội tuyến.
  • Thuộc tính có thể che giấu sự phức tạp. Đây là con dao hai lưỡi. Một mặt, nó làm cho mã của bạn dễ đọc và dễ thay đổi hơn. Nhưng mặt khác, một nhà phát triển khác truy cập vào một thuộc tính của lớp của bạn có thể nghĩ rằng họ chỉ đang truy cập vào một biến duy nhất và không nhận ra số lượng mã bạn thực sự ẩn đằng sau thuộc tính đó. Khi một tài sản bắt đầu trở nên đắt đỏ về mặt tính toán, thì tôi có xu hướng cấu trúc lại nó thành một phương thức GetFoo()/ rõ ràng SetFoo(value): Để ngụ ý rằng có rất nhiều điều xảy ra đằng sau hậu trường. (hoặc nếu tôi cần chắc chắn hơn nữa rằng những người khác nhận được gợi ý: CalculateFoo()/ SwitchFoo(value))

Truy cập một trường trong trường cấu trúc không chỉ đọc của loại cấu trúc là nhanh, ngay cả khi trường đó nằm trong cấu trúc nằm trong cấu trúc, v.v., với điều kiện đối tượng cấp cao nhất là lớp không chỉ đọc trường hoặc một biến đứng không chỉ đọc. Cấu trúc có thể khá lớn mà không ảnh hưởng xấu đến hiệu suất nếu những điều kiện này được áp dụng. Phá vỡ mô hình này sẽ gây ra sự chậm lại tỷ lệ thuận với kích thước cấu trúc.
supercat

5
"sau đó tôi có xu hướng tái cấu trúc nó thành một phương thức GetFoo () / SetFoo (value) rõ ràng:" - Điểm hay, tôi có xu hướng chuyển các hoạt động nặng ra khỏi các thuộc tính thành các phương thức.
Mark Rogers

Có cách nào để xác nhận rằng trình biên dịch Unity 3D thực hiện tối ưu hóa mà bạn đề cập (trong các bản dựng IL2CPP và Mono cho Android) không?
piojo

9
@piojo Giống như hầu hết các câu hỏi về hiệu suất, câu trả lời đơn giản nhất là "cứ làm đi". Bạn đã sẵn sàng mã - kiểm tra sự khác biệt giữa có một trường và có một thuộc tính. Các chi tiết thực hiện chỉ đáng để điều tra khi bạn thực sự có thể đo lường một sự khác biệt quan trọng giữa hai phương pháp. Theo kinh nghiệm của tôi, nếu bạn viết mọi thứ càng nhanh càng tốt, bạn hạn chế tối ưu hóa mức cao có sẵn và chỉ cố gắng chạy đua các phần cấp thấp ("không thể thấy rừng cho cây"). Ví dụ: tìm cách bỏ qua Updatehoàn toàn sẽ giúp bạn tiết kiệm nhiều thời gian hơn là làm Updatenhanh.
Luaan

9

Khi nghi ngờ, sử dụng thực hành tốt nhất.

Bạn đang nghi ngờ.


Đó là câu trả lời dễ dàng. Thực tế phức tạp hơn, tất nhiên.

Đầu tiên, có một huyền thoại rằng lập trình trò chơi là lập trình hiệu suất cực cao và mọi thứ phải nhanh nhất có thể. Tôi phân loại lập trình trò chơi là lập trình nhận thức hiệu suất . Nhà phát triển phải nhận thức được các hạn chế về hiệu suất và làm việc trong đó.

Thứ hai, có một huyền thoại rằng làm cho mọi thứ nhanh nhất có thể là những gì làm cho một chương trình chạy nhanh. Trong thực tế, việc xác định và tối ưu hóa các tắc nghẽn là điều làm cho chương trình nhanh và dễ dàng hơn / rẻ hơn / nhanh hơn nếu mã dễ duy trì và sửa đổi; mã cực kỳ tối ưu là khó để duy trì và sửa đổi. Theo sau đó để viết các chương trình cực kỳ tối ưu, bạn phải sử dụng tối ưu hóa mã cực kỳ thường xuyên khi cần thiết và hiếm khi có thể.

Thứ ba, lợi ích của các thuộc tính và các hàm truy cập là chúng rất mạnh để thay đổi, và do đó làm cho mã dễ duy trì và sửa đổi hơn - đó là điều bạn cần để viết các chương trình nhanh. Bạn muốn ném một ngoại lệ bất cứ khi nào ai đó muốn đặt giá trị của bạn thành null? Sử dụng một setter. Bạn muốn gửi thông báo bất cứ khi nào giá trị thay đổi? Sử dụng một setter. Bạn muốn đăng nhập giá trị mới? Sử dụng một setter. Bạn muốn làm lười khởi tạo? Sử dụng một getter.

Và thứ tư, cá nhân tôi không theo dõi thực tiễn tốt nhất của Microsoft trong trường hợp này. Nếu tôi cần một tài sản với các công cụ và setters tầm thường và công khai , tôi chỉ cần sử dụng một trường và chỉ cần thay đổi nó thành một tài sản khi tôi cần. Tuy nhiên, tôi rất hiếm khi cần một tài sản tầm thường như vậy - nó phổ biến hơn nhiều đến mức tôi muốn kiểm tra các điều kiện biên hoặc muốn đặt setter ở chế độ riêng tư.


2

Nó rất dễ dàng cho thời gian chạy .net đến các thuộc tính đơn giản nội tuyến, và thường thì nó. Tuy nhiên, nội tuyến này thường bị vô hiệu hóa bởi các trình biên dịch, do đó rất dễ bị nhầm lẫn và nghĩ rằng việc thay đổi một tài sản công cộng sang một lĩnh vực công cộng sẽ tăng tốc phần mềm.

Trong mã được gọi bởi mã khác sẽ không được biên dịch lại khi mã của bạn được biên dịch lại, có một trường hợp tốt để sử dụng các thuộc tính, vì thay đổi từ một trường thành một thuộc tính là một thay đổi vi phạm. Nhưng đối với hầu hết các mã, ngoài việc có thể đặt điểm dừng trên get / set, tôi chưa bao giờ thấy lợi ích khi sử dụng một thuộc tính đơn giản so với trường công khai.

Lý do chính khiến tôi sử dụng các thuộc tính trong 10 năm làm lập trình viên C # chuyên nghiệp là để ngăn các lập trình viên khác nói với tôi rằng tôi không tuân thủ tiêu chuẩn mã hóa. Đây là một sự đánh đổi tốt, vì hầu như không bao giờ có lý do chính đáng để không sử dụng tài sản.


2

Cấu trúc và trường trần sẽ giảm khả năng tương tác với một số API không được quản lý. Thường thì bạn sẽ thấy rằng API cấp thấp muốn truy cập các giá trị bằng cách tham chiếu, điều này tốt cho hiệu suất (vì chúng tôi tránh một bản sao không cần thiết). Sử dụng các thuộc tính là một trở ngại cho điều đó và thường thì các thư viện trình bao bọc sẽ thực hiện các bản sao để dễ sử dụng và đôi khi để bảo mật.

Do đó, bạn sẽ thường có hiệu suất tốt hơn khi có các loại vectơ và ma trận không có thuộc tính nhưng các trường trần trụi.


Thực hành tốt nhất là không tạo ra trong vắc-xin. Mặc dù một số giáo phái hàng hóa, nói chung thực hành tốt nhất là có lý do tốt.

Trong trường hợp này, chúng tôi có một cặp vợ chồng:

  • Một thuộc tính cho phép bạn thay đổi việc triển khai mà không thay đổi mã máy khách (ở cấp nhị phân, có thể thay đổi trường thành thuộc tính mà không thay đổi mã máy khách ở cấp nguồn, tuy nhiên, nó sẽ biên dịch sang thứ khác sau khi thay đổi) . Điều đó có nghĩa là, bằng cách sử dụng một thuộc tính từ đầu, mã tham chiếu của bạn sẽ không phải được biên dịch lại chỉ để thay đổi nội dung của thuộc tính đó.

  • Nếu không phải tất cả các giá trị có thể có của các trường thuộc loại của bạn là trạng thái hợp lệ, thì bạn không muốn đưa chúng ra mã máy khách mà mã đó sửa đổi nó. Do đó, nếu một số kết hợp các giá trị không hợp lệ, bạn muốn giữ các trường riêng tư (hoặc nội bộ).

Tôi đã nói mã khách hàng. Điều đó có nghĩa là mã gọi vào của bạn. Nếu bạn không làm thư viện (hoặc thậm chí làm thư viện mà sử dụng nội bộ thay vì công khai), bạn thường có thể thoát khỏi nó và một số kỷ luật tốt. Trong tình huống đó, cách tốt nhất để sử dụng các thuộc tính là có để ngăn bạn tự bắn vào chân mình. Hơn nữa, lý do về mã sẽ dễ dàng hơn nhiều nếu bạn có thể thấy tất cả các vị trí mà một trường có thể thay đổi trong một tệp duy nhất, thay vì phải lo lắng về bất cứ điều gì hay không nó đang được sửa đổi ở một nơi khác. Trong thực tế, các thuộc tính cũng tốt để đặt các điểm dừng khi bạn đang tìm ra điều gì sai.


Vâng, có giá trị là nhìn thấy những gì đang được thực hiện công nghiệp int. Tuy nhiên, bạn có động lực để đi ngược lại các thực tiễn tốt nhất không? hoặc bạn đang đi ngược lại các thực tiễn tốt nhất - làm cho mã khó lý luận hơn - chỉ vì ai đó đã làm điều đó? À, nhân tiện, "người khác làm việc đó" là cách bạn bắt đầu một giáo phái vận chuyển hàng hóa.


Vậy ... trò chơi của bạn có chạy chậm không? Bạn nên dành thời gian để tìm ra nút thắt cổ chai và khắc phục điều đó, thay vì suy đoán nó có thể là gì. Bạn có thể yên tâm rằng trình biên dịch sẽ thực hiện nhiều tối ưu hóa, vì điều đó, rất có thể bạn đang nhìn vào vấn đề sai.

Mặt khác, nếu bạn đang quyết định bắt đầu làm gì, bạn nên lo lắng về thuật toán và cấu trúc dữ liệu nào trước, thay vì lo lắng về các chi tiết nhỏ hơn như trường so với thuộc tính.


Cuối cùng, bạn có kiếm được một cái gì đó bằng cách đi ngược lại các thực tiễn tốt nhất?

Đối với các chi tiết của trường hợp của bạn (Unity và Mono cho Android), Unity có lấy giá trị bằng cách tham chiếu không? Nếu không, nó sẽ sao chép các giá trị bằng mọi giá, không có hiệu suất đạt được ở đó.

Nếu có, nếu bạn chuyển dữ liệu này tới API có tham chiếu. Liệu nó có ý nghĩa để làm cho trường công khai hay bạn có thể làm cho loại có thể gọi API trực tiếp?

Vâng, tất nhiên, có thể có những tối ưu hóa mà bạn có thể làm bằng cách sử dụng các cấu trúc với các trường trần. Ví dụ: bạn truy cập chúng bằng con trỏ Span<T> hoặc tương tự. Chúng cũng nhỏ gọn trên bộ nhớ, giúp chúng dễ dàng tuần tự hóa để gửi qua mạng hoặc lưu trữ vĩnh viễn (và vâng, đó là những bản sao).

Bây giờ, bạn đã chọn đúng thuật toán và cấu trúc, nếu chúng biến thành một nút cổ chai, thì bạn quyết định cách tốt nhất để khắc phục nó ... đó có thể là cấu trúc với các trường trần hay không. Bạn sẽ có thể lo lắng về điều đó nếu và khi nó xảy ra. Trong khi đó, bạn có thể lo lắng về những vấn đề quan trọng hơn như làm cho một trò chơi hay hoặc thú vị đáng chơi.


1

... Ấn tượng của tôi là trong quá trình phát triển trò chơi, trừ khi bạn có lý do để làm khác, mọi thứ nên nhanh nhất có thể.

:

Tôi biết rằng tối ưu hóa sớm là không tốt, nhưng như tôi đã nói, trong quá trình phát triển trò chơi, bạn không làm điều gì đó chậm chạp khi một nỗ lực tương tự có thể làm cho nó nhanh.

Bạn có quyền nhận thức được rằng tối ưu hóa sớm là xấu nhưng đó chỉ là một nửa câu chuyện (xem trích dẫn đầy đủ bên dưới). Ngay cả trong phát triển trò chơi, không phải tất cả mọi thứ cần phải nhanh nhất có thể.

Đầu tiên làm cho nó hoạt động. Chỉ sau đó, tối ưu hóa các bit cần nó. Điều đó không có nghĩa là bản thảo đầu tiên có thể lộn xộn hoặc không hiệu quả nhưng tối ưu hóa không phải là ưu tiên trong giai đoạn này.

" Vấn đề thực sự là các lập trình viên đã dành quá nhiều thời gian để lo lắng về hiệu quả ở những vị trí sai và không đúng lúc ; tối ưu hóa sớm là gốc rễ của mọi tội lỗi (hoặc ít nhất là phần lớn) trong lập trình." Donald Knuth, 1974.


1

Đối với những thứ cơ bản như thế, trình tối ưu hóa C # sẽ làm cho nó đúng. Nếu bạn lo lắng về điều đó (và nếu bạn lo lắng về nó hơn 1% mã của mình, thì bạn đã làm sai ) một thuộc tính đã được tạo cho bạn. Thuộc tính [MethodImpl(MethodImplOptions.AggressiveInlining)]làm tăng đáng kể khả năng một phương thức sẽ được nội tuyến (thuộc tính getters và setters là phương thức).


0

Phơi bày các thành viên kiểu cấu trúc như các thuộc tính chứ không phải các trường thường sẽ mang lại hiệu suất và ngữ nghĩa kém, đặc biệt là trong trường hợp các cấu trúc thực sự được coi là cấu trúc, thay vì là "các đối tượng kỳ quặc".

Một cấu trúc trong .NET là một tập hợp các trường được nối với nhau và có thể được coi là một đơn vị. .NET Framework được thiết kế để cho phép các cấu trúc được sử dụng theo cách tương tự như các đối tượng lớp và đôi khi điều này có thể thuận tiện.

Phần lớn lời khuyên xung quanh các cấu trúc được đưa ra dựa trên quan niệm rằng các lập trình viên sẽ muốn sử dụng các cấu trúc như các đối tượng, thay vì các bộ sưu tập các trường kết hợp với nhau. Mặt khác, có nhiều trường hợp có thể hữu ích khi có một cái gì đó hoạt động như một bó các lĩnh vực được dán lại với nhau. Nếu đó là những gì người ta cần, lời khuyên .NET sẽ bổ sung công việc không cần thiết cho các lập trình viên và trình biên dịch, mang lại hiệu suất và ngữ nghĩa kém hơn so với việc sử dụng trực tiếp các cấu trúc.

Các nguyên tắc lớn nhất cần ghi nhớ khi sử dụng các cấu trúc là:

  1. Không vượt qua hoặc trả lại các cấu trúc theo giá trị hoặc làm cho chúng bị sao chép không cần thiết.

  2. Nếu nguyên tắc số 1 được tuân thủ, các cấu trúc lớn sẽ hoạt động tốt như các cấu trúc nhỏ.

Nếu Alphabloblà một cấu trúc chứa 26 inttrường công khai có tên az và một getter abthuộc tính trả về tổng của các trường abcác trường của nó , thì Alphablob[] arr; List<Alphablob> list;int foo = arr[0].ab + list[0].ab;sẽ cần đọc các trường abcủa arr[0], nhưng sẽ cần đọc tất cả 26 trường list[0]mặc dù nó sẽ bỏ qua tất cả nhưng hai trong số họ. Nếu một người muốn có một bộ sưu tập giống như danh sách chung có thể hoạt động hiệu quả với cấu trúc như thế alphaBlob, thì ta nên thay thế getter được lập chỉ mục bằng một phương thức:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

mà sau đó sẽ gọi proc(ref backingArray[index], ref param);. Cho một bộ sưu tập như vậy, nếu một thay thế sum = myCollection[0].ab;bằng

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

điều đó sẽ tránh sự cần thiết phải sao chép các phần alphaBlobkhông được sử dụng trong trình nhận tài sản. Vì đại biểu được thông qua không truy cập bất cứ thứ gì ngoài reftham số của nó , trình biên dịch có thể truyền một đại biểu tĩnh.

Thật không may, .NET Framework không định nghĩa bất kỳ loại đại biểu nào cần thiết để làm cho điều này hoạt động tốt và cú pháp kết thúc là một mớ hỗn độn. Mặt khác, cách tiếp cận này cho phép tránh sao chép các cấu trúc một cách không cần thiết, điều này sẽ giúp thực hiện các hành động tại chỗ trên các cấu trúc lớn được lưu trữ trong các mảng, đây là cách truy cập lưu trữ hiệu quả nhất.


Xin chào, bạn đã gửi câu trả lời của bạn đến trang sai?
piojo

@pojo: Có lẽ tôi nên nói rõ hơn rằng mục đích của câu trả lời là xác định một tình huống sử dụng các thuộc tính thay vì các trường là xấu và xác định cách người ta có thể đạt được hiệu suất và lợi thế ngữ nghĩa của các trường, trong khi vẫn đóng gói quyền truy cập.
supercat

@piojo: Chỉnh sửa của tôi có làm mọi thứ rõ ràng hơn không?
supercat

"sẽ cần phải đọc tất cả 26 trường của danh sách [0] mặc dù nó sẽ bỏ qua tất cả trừ hai trong số chúng." gì? Bạn có chắc không? Bạn đã kiểm tra MSIL chưa? C # gần như luôn luôn vượt qua các loại lớp bằng cách tham chiếu.
pjc50

@ pjc50: Đọc một tài sản mang lại kết quả theo giá trị. Nếu cả getter được lập chỉ mục cho listvà getter abthuộc tính đều được xếp hàng, thì JITter có thể nhận ra rằng chỉ có các thành viên ablà cần thiết, nhưng tôi không biết bất kỳ phiên bản JITter nào thực sự làm những việc như vậy.
supercat
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.