2

I have the following class hierarchy of type Item:

Item

  • Weapon : Item

  • Armor : Item

I use a generic linked list of type Item. I have inserted different kinds of items: Armor, Weapon, etc. I stuck to two item types in this example.

I have the following code:

// Create the map. The underneath data structure is: 
// Array2D<LinkedList<Item>> m_map;
// A generic 2d array contains a linked list of type cell at each cell.
// This makes it easy to drop and pick up items at a given player position.
Map map = new Map(10, 10);

// First, I store two items at position (0,0) on my map.
map.AddItem(0, 0, new Weapon(6));
map.AddItem(0, 0, new Armor(3));

// Now, this only retrieves weapons. 
var qItems = from w in map.GetItems(0, 0)
        where w is Weapon
        select w;

// Weapons are outputted
foreach (Weapon weapon in qItems)
    Console.WriteLine(weapon.m_damage);

This works fine, but when I'm interested in gathering all items (which will be often), I have to do this:

// All items are outputted
foreach (Item item in map.GetItems(0, 0))
{
    if (item is Weapon) 
        Console.WriteLine("Damage: {0}", (item as Weapon).m_damage); 
    else if (item is Armor)
        Console.WriteLine("AC: {0}", (item as Armor).m_ac);
} 

Is there a more efficient way of retrieving different item types than using is/as? I think this can lead to bugs because what if a developer adds a new item type but forgets to make a condition for it in the foreach block? As you can guess, the list can get extensive of different types (10 or so in this project).

Edit: This is not a duplicate question. I'm looking at all subclasses, while the possible duplicate message is focused on retrieving one subclass.

Bob
  • 115
  • 10
  • One thing I should mention: Where you have written `if (item is Weapon) Console.WriteLine(..., (item as Weapon).m_damage)` can be easily changed to `if (item is Weapon w) Console.WriteLine(..., w.m_damage)`. – Frontear Apr 06 '18 at 03:09
  • 1
    @Frontear Depends on which c# version they are using – Hasan Emrah Süngü Apr 06 '18 at 03:10
  • 1
    Wow, I never knew you could do that! Much easier. Thanks for sharing. I'm using C# 7.2. – Bob Apr 06 '18 at 03:11
  • @EmrahSüngü Correct me if I am wrong, but according to this [MSDN Article](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/is), the `is` keyword was added in `C#` 7, alongside the ability to pattern match a variable. – Frontear Apr 06 '18 at 03:13
  • @Frontear, Yeah exactly what i am saying :) Depends on the c# version OP is using. – Hasan Emrah Süngü Apr 06 '18 at 03:14
  • @EmrahSüngü If they are using the `is` keyword in their example, then they must be using `C#` 7 or higher.. – Frontear Apr 06 '18 at 03:15
  • 2
    @Frontear No that is not correct. `is` keyword has been in the framework for a very long time. Pattern matching with `is` keyword is from `c# 7`. You can easily test that. – Hasan Emrah Süngü Apr 06 '18 at 03:16
  • @EmrahSüngü That is news to me, thanks for letting me know. – Frontear Apr 06 '18 at 03:17
  • 1
    Can't you define a method in `Item`, say `PrintInfo()`, that the inheriting classes have their own implementation? So you don't have to explicitly cast them. So inside your foreach, you can directly call `item.PrintInfo()` – ragyaiddo Apr 06 '18 at 03:18
  • My C# 4.0 book uses is/an, but it may have been earlier than that using is/as. – Bob Apr 06 '18 at 03:23
  • Oh, like a virtual function? That's a good idea. – Bob Apr 06 '18 at 03:24
  • Here's one way you can implement it: https://dotnetfiddle.net/ndSjsx – ragyaiddo Apr 06 '18 at 03:25
  • Good idea. That further streamlines it. – Bob Apr 06 '18 at 03:26
  • Possible duplicate of [LINQ: From a list of type T, retrieve only objects of a certain subclass S](https://stackoverflow.com/questions/1184944/linq-from-a-list-of-type-t-retrieve-only-objects-of-a-certain-subclass-s) – Mick Apr 06 '18 at 03:29
  • @Mick Op does have a different problem, I think – Hasan Emrah Süngü Apr 06 '18 at 03:30
  • @EmrahSüngü What's the difference? – Mick Apr 06 '18 at 03:32
  • @Mick I do not see how filtering certain types of objects can help with gathering all types of objects? – Hasan Emrah Süngü Apr 06 '18 at 03:33
  • @EmrahSüngü perhaps I'm not understanding the semantics of "gathering" – Mick Apr 06 '18 at 03:37
  • @Mick, I apologize for not being clear. OP accesses all items at a position, which are held in a linked list. As stated in the question, all items inherit a base class `Item` and obviously `LinkedList` is of type `Item`. Using OOP techniques how can OP print some specific info about all derived classes. – Hasan Emrah Süngü Apr 06 '18 at 03:41
  • All of these are great answers. I'll see which works best as the project gets more complex. – Bob Apr 06 '18 at 04:09

5 Answers5

4

In oop we rely on virtual dispatch to do our bidding so I usually do not use is keyword to determine the type of the object. I believe if you have an inheritance similar to the following then you can do

// items are outputted
foreach (var item in qItems)
    Console.WriteLine(item.ShowStats());
}

Item inheritance:

public abstract class Item {

    public abstract string ShowStats();
}

Then armor will be

public class Armor : Item {
    private readonly double _armor;

    public Armor(double armor) {
        _armor = armor;
    }

    public override string ShowStats() {
        return $"Armor: {_armor}";
    }
}

And weapon will be

public class Weapon : Item {
    private readonly double _damage;

    public Weapon(double damage) {
        _damage = damage;
    }

    public override string ShowStats() {
        return $"Damage: {_damage}";
    }
}
Hasan Emrah Süngü
  • 3,488
  • 1
  • 15
  • 33
  • Interesting. I never thought of using 'var' in foreach. I tried plain old 'Item' instead of 'var' before and had issues. Var gets rid of these conversion problems. I think the reason is var obtains a yielded object with IEnumerable, but I could be wrong. – Bob Apr 06 '18 at 03:57
  • @Bob whenever compiler can determine the type of object you can use `var` – Hasan Emrah Süngü Apr 06 '18 at 04:00
2

The reason you can't really do this is because of the following reason:

While you could use an example like this to find all the classes that inherit Item, you can't really make this automated because each class needs to access a different variable.

Take for example, you code that you posted:

// All items are outputted
foreach (Item item in map.GetItems(0, 0))
{
    if (item is Weapon) 
        Console.WriteLine("Damage: {0}", (item as Weapon).m_damage); 
    else if (item is Armor)
        Console.WriteLine("AC: {0}", (item as Armor).m_ac);
} 

In this code, you access the item as Weapon field m_damage. This, however, changes with the next line, as instead of finding field m_damage, you look for m_ac inside of item as Armor. It simply can't be automated beyond actually defining which classes item can be a Type of.

One suggestion I can make is that you could create properties within the Item class which contain m_damage and m_ac, then just override them in your inherited classes. This will allow you to access these fields without needing to check their Type.

public class Item
{
    public virtual int m_damage { get; set; }
}

public class Weapon : Item
{
    public override int m_damage => 4;
}

Alternatively, a better solution, as @ragyaiddo pointed out is to create a method within the Item class called PrintInfo(), which is then override into the inherited classes, which will simply print out information about that Item.

public abstract class Item
{
    public abstract void PrintStats();
}

public class Weapon : Item
{
    public int Damage { get; set; }

    public override void PrintStats()
    {
        Console.WriteLine("Damage: {0}", Damage);
    }
}
Frontear
  • 1,150
  • 12
  • 25
  • On creating properties in the Item class: Yes, thought I'd have to convert using 'is' to see if the property is a valid one to obtain. Otherwise an invalid property on type 'Sword,' such as 'armor class,' returns 0. There are several common properties though like 'weight,' and the such. – Bob Apr 06 '18 at 04:21
1

The visitor pattern is often used in this kind of situation. But you will always have something that resolves the subclasses

Your alternative is to build Items in such a way as they know what to do

So you just do Console.WriteLine(item.Stats)

Stats could be a new class ItemStats that is flexible and could store many keyvalue pairs of stats

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
Keith Nicholas
  • 43,549
  • 15
  • 93
  • 156
  • Do you mean ItemStats is just one class holding all stats instead of 10+ subclasses? – Bob Apr 06 '18 at 02:50
  • well possibly, in OO, replacing subclassing with a wayyou can configure things to get variations is good ( which is part of composition over inheritence ) . If the only thing your subclasses are doing is providing stats then this is a good option. But if they also provide new behaviours a different approach might be needed. Id probablly compose things with stats and behaviors – Keith Nicholas Apr 06 '18 at 02:54
  • I can imagine needing that flexibility later for composing stats and behavior. Think I should just keep it as it is then? – Bob Apr 06 '18 at 03:05
  • I have no idea. That's a much bigger question to do with your overall design. if you have 10+ subclasses then you have to work out how they all interact with each other then you may find your design as it is very clunky and difficult to work with – Keith Nicholas Apr 06 '18 at 03:13
  • On a first glance, I think sub classes help keep things organized a bit. Otherwise there's a ton of conditions to test in one class before presenting or returning stats. I guess I'll have to see how it goes and change it later if need be. I do like this as an alternative. – Bob Apr 06 '18 at 03:31
1

As I pointed out in the comment, you can define an abstract method in Item class and have the inheriting classes implement their specific implementation. I created a sample implementation here. This is a similar approach to @Emrah Süngü's solution.

You can take this further with the implementation of Template Method Pattern or Strategy Pattern, depending on your requirements. These patterns are applicable when you have more than one class that need to follow the same common workflow, but each of these classes has different internal implementations of the said workflow.

ragyaiddo
  • 152
  • 1
  • 4
0

As answered here using linq...

foreach (Weapon item in map.GetItems(0, 0).OfType<Weapon>())
    Console.WriteLine("Damage: {0}", item.m_damage); 

foreach (Armor item in map.GetItems(0, 0).OfType<Armor>())
    Console.WriteLine("AC: {0}", item.m_ac);

Alternatively...

public class Item { }
public class Weapon : Item
{
    public int Damage { get; set; }

    public override string ToString()
    {
        return $"Damage: {Damage}";
    }
}

public class Armor : Item
{
    public int AC { get; set; }

    public override string ToString()
    {
        return $"AC: {AC}";
    }
}

foreach (Item item in map.GetItems(0, 0))
    Console.WriteLine(item.ToString());
Mick
  • 6,527
  • 4
  • 52
  • 67