[C#] لماذا لا ترث من القائمة <T>؟


Answers

وأخيرًا ، يقترح البعض التفاف القائمة في شيء ما:

هذه هي الطريقة الصحيحة. "كلمة بلا معنى" هي طريقة سيئة للنظر إلى هذا. لها معنى واضح عند كتابة my_team.Players.Count . تريد عد اللاعبين.

my_team.Count

..لا يعني شيئا. عد ماذا؟

الفريق ليس قائمة - تتكون من أكثر من مجرد قائمة من اللاعبين. يمتلك الفريق لاعبين ، لذا يجب أن يكون اللاعبون جزءًا منه (عضو).

إذا كنت قلقًا حقًا من أن تكون مطوّلًا بشكل مفرط ، فيمكنك دومًا عرض الخصائص من الفريق:

public int PlayerCount {
    get {
        return Players.Count;
    }
}

.. الذي يصبح:

my_team.PlayerCount

هذا له معنى الآن ويلتزم قانون ديميتر .

يجب أن تفكر أيضًا في الالتزام بمبدأ إعادة الاستخدام المركب . من خلال الإرث من List<T> ، فأنت تقول أن الفريق عبارة عن قائمة باللاعبين ويكشف عن الطرق غير الضرورية للخروج منه. هذا غير صحيح - كما ذكرتم ، فإن الفريق أكثر من مجرد قائمة من اللاعبين: فهو يحمل اسمًا ، أو مديرًا ، أو أعضاء مجلس إدارة ، أو مدربين ، أو طاقم طبي ، أو قبعات راتب ، إلخ. من خلال جعل صف فريقك يحتوي على قائمة اللاعبين ، يقول "الفريق يحتوي على قائمة اللاعبين" ، ولكن يمكن أن يكون له أيضًا أشياء أخرى.

Question

عند التخطيط لبرامجي ، غالباً ما أبدأ بسلسلة من الأفكار مثل:

فريق كرة القدم هو مجرد قائمة لاعبي كرة القدم. لذلك ، يجب عليّ تمثيلها مع:

var football_team = new List<FootballPlayer>();

يمثل ترتيب هذه القائمة الترتيب الذي يتم فيه إدراج اللاعبين في القائمة.

لكني أدرك لاحقاً أن الفرق لديها خصائص أخرى ، بالإضافة إلى قائمة اللاعبين ، يجب تسجيلها. على سبيل المثال ، إجمالي عدد النتائج هذا الموسم ، الميزانية الحالية ، الألوان الموحدة ، string تمثل اسم الفريق ، وما إلى ذلك.

إذن أعتقد:

حسناً ، إن فريق كرة القدم هو تماماً مثل قائمة اللاعبين ، ولكن بالإضافة إلى ذلك ، فإنه يحتوي على اسم ( string ) ومجموع كلي من الدرجات ( int ). لا توفر .NET فئة لتخزين فرق كرة القدم ، لذا سأقدم دروسي الخاص. إن البنية القائمة الأكثر تشابهاً وذات صلة هي List<FootballPlayer> ، لذا سأرثها:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

ولكن اتضح أن هناك مبدأ توجيهي يجب ألا ترثه من List<T> . أنا مرتبك تماما من هذا المبدأ التوجيهي في ناحيتين.

لما لا؟

List ما يبدو هو الأمثل للأداء بطريقة أو بأخرى . كيف ذلك؟ ما مشاكل الأداء التي سأسببها في حالة توسيع List ؟ ما سوف يكسر بالضبط؟

السبب الآخر الذي رأيته هو أن List يتم توفيرها بواسطة Microsoft ، وليس لديّ أي تحكم فيها ، لذا لا يمكنني تغييرها لاحقًا ، بعد تعريض "واجهة برمجة تطبيقات عامة" . لكنني أكافح لفهم هذا. ما هي واجهة برمجة التطبيقات العامة ولماذا يجب أن أهتم؟ إذا كان مشروعي الحالي لا يملك واجهة برمجة تطبيقات عامة على الأرجح ، فهل يمكنني تجاهل هذا المبدأ التوجيهي بأمان؟ إذا ورثت من List واتضح أنني بحاجة إلى واجهة برمجة تطبيقات عامة ، فما الصعوبات التي سأواجهها؟

لماذا لا يهم حتى؟ القائمة هي قائمة. ما الذي يمكن أن يتغير؟ ما الذي يمكن أن أرغب في تغييره؟

وأخيرًا ، إذا لم تكن مايكروسوفت تريد مني أن أرث من List ، فلماذا لم تجعل الطبقة sealed ؟

ماذا أنا من المفترض أن تستخدم؟

على ما يبدو ، بالنسبة للمجموعات المخصصة ، قامت Microsoft بتوفير فئة Collection يجب تمديدها بدلاً من List . لكن هذه الطبقة عارية للغاية ، وليس لديها العديد من الأشياء المفيدة ، مثل AddRange ، على سبيل المثال. توفر إجابة jvitor83 أساسًا منطقيًا للأداء لهذا الأسلوب المعين ، ولكن كيف يكون معدل AddRange بطيئًا وليس أفضل من AddRange ؟

إن التوارث من Collection هو عمل أكثر من الإرث من List ، ولا أرى فائدة. من المؤكد أن مايكروسوفت لم تطلب مني القيام بعمل إضافي دون سبب ، لذلك لا يسعني أن أشعر بأنني أخطئ في فهم شيء ما ، وأن وراثة Collection ليست الحل الصحيح لمشكلتي.

لقد رأيت اقتراحات مثل تطبيق IList . فقط لا. هذا هو عشرات الأسطر من التعليمات البرمجية الأساسية التي لا تكسب لي شيئا.

وأخيرًا ، يقترح البعض التفاف List في شيء ما:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

هناك مشكلتان مع هذا:

  1. يجعل كود بلادي مطول بلا داع. يجب أن أتصل الآن my_team.Players.Count بدلاً من my_team.Count فقط. الحمد لله ، مع C # يمكنني تحديد مفهرسات لجعل الفهرسة شفافة ، وإلى الأمام جميع أساليب القائمة الداخلية ... ولكن هذا كثير من التعليمات البرمجية! ماذا أحصل على كل هذا العمل؟

  2. انها مجرد لا معنى له. لا يملك فريق كرة القدم "قائمة" باللاعبين. إنها قائمة اللاعبين. لا تقول "انضم جون ماكفوت بولر إلى لاعبي SomeTeam". أنت تقول "جون قد انضم إلى SomeTeam". لا تضيف حرفًا إلى "أحرف السلسلة" ، فأنت تضيف حرفًا إلى سلسلة. لا تضيف كتابًا إلى كتب المكتبة ، بل تضيف كتابًا إلى المكتبة.

أدرك أن ما يحدث "تحت غطاء المحرك" يمكن أن يقال أنه "يضيف قائمة X إلى Y الداخلية" ، لكن هذا يبدو وكأنه طريقة تفكير غير بديهية للغاية حول العالم.

سؤالي (ملخص)

ما هي الطريقة الصحيحة C # لتمثيل بنية البيانات ، والتي ، "منطقياً" (أي "للعقل البشري") هي مجرد list things مع القليل من الأجراس والصفارات؟

هل الموروث من List<T> دائمًا غير مقبول؟ متى يكون مقبولا؟ لماذا لماذا لا؟ ماذا يجب أن يفكر المبرمج ، عند تحديد ما إذا كان سيرث من List<T> أم لا؟




Let me rewrite your question. so you might see the subject from a different perspective.

When I need to represent a football team, I understand that it is basically a name. Like: "The Eagles"

string team = new string();

Then later I realized teams also have players.

Why can't I just extend the string type so that it also holds a list of players?

Your point of entry into the problem is arbitrary. Try to think what does a team have (properties), not what it is .

After you do that, you could see if it shares properties with other classes. And think about inheritance.




What is the correct C# way of representing a data structure...

Remeber, "All models are wrong, but some are useful." - George EP Box

There is no a "correct way", only a useful one.

Choose one that is useful to you and/your users. هذا هو. Develop economically, don't over-engineer. The less code you write, the less code you will need to debug. (read the following editions).

-- Edited

My best answer would be... it depends. Inheriting from a List would expose the clients of this class to methods that may be should not be exposed, primarily because FootballTeam looks like a business entity.

-- Edition 2

I sincerely don't remember to what I was referring on the “don't over-engineer” comment. While I believe the KISS mindset is a good guide, I want to emphasize that inheriting a business class from List would create more problems than it resolves, due abstraction leakage .

On the other hand, I believe there are a limited number of cases where simply to inherit from List is useful. As I wrote in the previous edition, it depends. The answer to each case is heavily influenced by both knowledge, experience and personal preferences.

Thanks to @kai for helping me to think more precisely about the answer.




It depends on the behaviour of your "team" object. If it behaves just like a collection, it might be OK to represent it first with a plain List. Then you might start to notice that you keep duplicating code that iterates on the list; at this point you have the option of creating a FootballTeam object that wraps the list of players. The FootballTeam class becomes the home for all the code that iterates on the list of players.

It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count. Thankfully, with C# I can define indexers to make indexing transparent, and forward all the methods of the internal List... But that's a lot of code! What do I get for all that work?

Encapsulation. Your clients need not know what goes on inside of FootballTeam. For all your clients know, it might be implemented by looking the list of players up in a database. They don't need to know, and this improves your design.

It just plain doesn't make any sense. A football team doesn't "have" a list of players. It is the list of players. You don't say "John McFootballer has joined SomeTeam's players". You say "John has joined SomeTeam". You don't add a letter to "a string's characters", you add a letter to a string. You don't add a book to a library's books, you add a book to a library.

Exactly :) you will say footballTeam.Add(john), not footballTeam.List.Add(john). The internal list will not be visible.




This reminds me of the "Is a" versus "has a" tradeoff. Sometimes it is easier and makesmore sense to inherit directly from a super class. Other times it makes more sense to create a standalone class and include the class you would have inherited from as a member variable. You can still access the functionality of the class but are not bound to the interface or any other constraints that might come from inheriting from the class.

Which do you do? As with a lot of things...it depends on the context. The guide I would use is that in order to inherit from another class there truly should be an "is a" relationship. So if you a writing a class called BMW, it could inherit from Car because a BMW truly is a car. A Horse class can inherit from the Mammal class because a horse actually is a mammal in real life and any Mammal functionality should be relevant to Horse. But can you say that a team is a list? From what I can tell, it does not seem like a Team really "is a" List. So in this case, I would have a List as a member variable.




بادئ ذي بدء ، فإنه يجب القيام به مع سهولة الاستخدام. إذا كنت تستخدم الوراثة ، فستقوم فئة Team بكشف السلوك (الطرق) المصممة تمامًا لمعالجة الكائن. على سبيل المثال ، AsReadOnly() أو CopyTo(obj) لا معنى لـ كائن الفريق. بدلاً من أسلوب AddRange(items) قد ترغب على الأرجح في أسلوب AddPlayers(players) أكثر AddPlayers(players) .

إذا كنت ترغب في استخدام LINQ ، فإن تنفيذ واجهة عامة مثل ICollection<T> أو IEnumerable<T> سيكون أكثر منطقية.

كما ذكرنا ، فإن التكوين هو الطريقة الصحيحة للتقدم. فقط قم بتطبيق قائمة اللاعبين كمتغير خاص.




class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

الرمز السابق يعني: مجموعة من اللاعبين من الشارع يلعبون كرة القدم ، ويحدث لهم اسم. شيء مثل:

على أي حال ، هذا الرمز (من جوابي)

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
    get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

يعني: هذا هو فريق كرة القدم الذي لديه الإدارة ، واللاعبين ، والمشرفين ، الخ. شيء من هذا القبيل:

هذه هي الطريقة التي يقدم بها المنطق في الصور ...




What the guidelines say is that the public API should not reveal the internal design decision of whether you are using a list, a set, a dictionary, a tree or whatever. A "team" is not necessarily a list. You may implement it as a list but users of your public API should use you class on a need to know basis. This allows you to change your decision and use a different data structure without affecting the public interface.




A football team is not a list of football players. A football team is composed of a list of football players!

This is logically wrong:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

and this is correct:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}



كما أشار الجميع ، فإن فريق من اللاعبين ليس قائمة باللاعبين. يرتكب هذا الخطأ العديد من الناس في كل مكان ، ربما على مستويات مختلفة من الخبرة. غالبًا ما تكون المشكلة دقيقة وأحيانًا شديدة جدًا ، كما هو الحال في هذه الحالة. هذه التصاميم سيئة لأنها تنتهك مبدأ استبدال Liskov . يحتوي الإنترنت على العديد من المقالات الجيدة التي توضح هذا المفهوم ، على سبيل المثال ، http://en.wikipedia.org/wiki/Liskov_substitution_principle

باختصار ، هناك قاعدتان يجب حفظهما في علاقة الوالد / الطفل بين الفئات:

  • يجب أن لا يتطلب الطفل أي خاصية أقل من تحديد الوالدين كليًا.
  • يجب أن لا يتطلب الوالد سمة مميزة بالإضافة إلى تعريف الطفل تمامًا.

وبعبارة أخرى ، فإن الوالدين هو تعريف ضروري للطفل ، والطفل هو تعريف كافٍ لأحد الوالدين.

هنا طريقة للتفكير من خلال حل واحد وتطبيق المبدأ السابق الذي ينبغي أن يساعد المرء على تجنب مثل هذا الخطأ. يجب اختبار الفرضية من خلال التحقق مما إذا كانت جميع عمليات فئة الأصل صالحة للطبقات المشتقة من الناحية الهيكلية والدلالية.

  • هل فريق كرة القدم قائمة لاعبي كرة القدم؟ (هل تنطبق جميع خصائص القائمة على فريق بنفس المعنى)
    • هو فريق مجموعة من الكيانات المتجانسة؟ نعم ، الفريق عبارة عن مجموعة من اللاعبين
    • هل ترتيب إدراج اللاعبين وصفًا لحالة الفريق وهل يضمن الفريق الحفاظ على التسلسل ما لم يتم تغييره صراحة؟ لا و ​​لا
    • هل من المتوقع أن يتم تضمين / إسقاط اللاعبين بناءً على موقعهم المتسلسل في الفريق؟ لا

كما ترى ، لا تنطبق سوى الميزة الأولى للقائمة على الفريق. وبالتالي ، فإن الفريق ليس قائمة. ستكون القائمة عبارة عن تفاصيل تنفيذية لكيفية إدارة فريقك ، لذلك يجب استخدامه فقط لتخزين كائنات المشغل والتلاعب بطرق لفصل الفريق.

في هذه المرحلة ، أود أن أشير إلى أنه ينبغي ، في رأيي ، عدم تطبيق فئة فريق باستخدام القائمة ؛ يجب أن يتم تنفيذ ذلك باستخدام بنية البيانات Set (HashSet ، على سبيل المثال) في معظم الحالات.




If your class users need all the methods and properties** List has, you should derive your class from it. If they don't need them, enclose the List and make wrappers for methods your class users actually need.

This is a strict rule, if you write a public API , or any other code that will be used by many people. You may ignore this rule if you have a tiny app and no more than 2 developers. This will save you some time.

For tiny apps, you may also consider choosing another, less strict language. Ruby, JavaScript - anything that allows you to write less code.




While I don't have a complex comparison as most of these answers do, I would like to share my method for handling this situation. By extending IEnumerable<T> , you can allow your Team class to support Linq query extensions, without publicly exposing all the methods and properties of List<T> .

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}