-2

I have a generic class with the following constructors 1. Map(int resolution), 2. Map(int resolution, T defaultValue), 3. Map(int width, int height) and 4. Map(int width, int height, T defaultValue), what happens if one of the inheriting classes requires map<int>, constructors 2 and 3 each have 2 parameters and in the map<int> case they would both be integers.

public abstract class Map<T> {
    private T[,] m_Map;
    private T m_DefaultValue;

    public T this[int x, int y] {
        get {
            return InRange(x,y)?m_Map[x,y]:DefaultValue;
        }
        set {
            if(InRange(x,y)) { // Included to handle confusion between retrieving a non-existent position, and setting one.
                m_Map[x,y]  value;
            }
        }
    }

    public int Width{get { return m_Map.GetLength(0); } }
    public int Height{get { return m_Map.GetLength(1); } }
    public virtual T DefaultValue { get { return m_DefaultValue; } set{ m_DefaultValue = value; } }

    public Map(int resolution) : this(resolution, resolution, default) {}
    public Map(int resolution, T defaultValue) : this(resolution, resolution, defaultValue) {}
    public Map(int width, int height) : this(width, height, default) {}
    public Map(int width, int height, T defaultValue) {
        m_Map = new T[width, height];
        m_DefaultValue = defaultValue;
    }

    public bool InRange(int x, int y) {
        return x>=0&&x<m_Map.GetLength(0)&&y>=0&&y<m_Map.GetLength(1);
    }
}
  • 5
    You could find out ... – 500 - Internal Server Error Dec 29 '21 at 22:28
  • Show us the code for your generic class. – Robert Harvey Dec 29 '21 at 22:28
  • 1
    Show the class as a block of code. Descriptions require us to visualize what you already have as code. That's unnecessary mental overhead, which is not respectful of the effort we put into reading your question. – madreflection Dec 29 '21 at 22:28
  • 2
    Test the code, or ask the internet. Which one do you think demonstrates a basic level of research. – Anthony Pegram Dec 29 '21 at 22:29
  • @AnthonyPegram It is true, that I could test the code to find out the what, but I wouldn't know or understand the why and would be unable to even attempt to make necessary adjustments to handle these "Corner" cases –  Dec 29 '21 at 22:42
  • I would remove the constructors that take `int resolution` since those are just shortcuts to the constructors that take `int width, int height`. If you need the shortcuts, provide factory methods. – madreflection Dec 29 '21 at 22:43
  • In c#, [constructors are not inherited](https://stackoverflow.com/a/4544416/2791540). If one of the descendent classes needs the functionality of the base class' constructors, it can call them using the [base](https://stackoverflow.com/a/2644334/2791540) keyword, or extract the functionality to a non-constructor method (which, unlike constructors, *are* inherited). – John Wu Dec 29 '21 at 22:43
  • @JohnWu: The `base()` call does have to go through overload resolution, though, so if `T` is `int` and you pass two `int` values to it, that's what OP is trying to understand.... whether (and why) it calls `int resolution, T(int) defaultValue` or `int width, int height`. – madreflection Dec 29 '21 at 22:44
  • @madreflection I also considered removing the parameter for the defaultValue, and setting it to default, requiring the user to set it after the initialization if they require a value other than default (eg one of my implementations is going to be for a map and the default/OutOfRange value needs to be true.) –  Dec 29 '21 at 22:49
  • In addition to the answers below, take a look at [this](https://stackoverflow.com/q/3678703) and [this](https://stackoverflow.com/q/18339503). I would avoid having the default as a settable property because that implies that a mid-use change is valid when in fact it could cause unexpected behavior. An init-only property might be OK, though. – madreflection Dec 29 '21 at 22:52
  • I wanted a mid-use change to be accepted, as any method that requires a map object of a specific type may have different requirements for what the default value should be if the requested index is out of range. (I also realize now, that having map be a class won't work in this case, it'll need to be changed to a struct for this functionality) –  Dec 29 '21 at 22:56
  • For the case you just described, the methods that return a value should accept the default as a parameter. The default is call-specific. Making it part of the object's state is just asking for bugs related to state management at unrelated call sites. – madreflection Dec 29 '21 at 22:57
  • @madreflection then perhaps the best solution at least in this case, would be to take default value as a parameter of the indexer, allowing it to be left out (preset to default)? –  Dec 29 '21 at 23:00
  • An indexer is also the wrong design choice for that. The default value only applies to the `get` accessor, so it would have to be part of the `set` accessor's signature while providing absolutely no value whatsoever. – madreflection Dec 29 '21 at 23:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/240554/discussion-between-rwolfe-and-madreflection). –  Dec 29 '21 at 23:41

2 Answers2

1

You can use named arguments to disambiguate the methods.

//Call Map(int,int)
var x = new Map<int>(width: 1, height: 2);

//Call Map(int, T)
var x = new Map<int>(width: 1, defaultValue: 3);

The same will work with the base keyword.

class MyClass : Map<int>
{
    public MyClass(int width, int defaultValue) : base( width: width, defaultValue: defaultValue)
    {
    }
}
John Wu
  • 50,556
  • 8
  • 44
  • 80
1

This is technically possible in the CLI. The ECMA-335 specification specifically points this out:

II.9.8 Signatures and binding

It is possible for distinct members to have identical types when instantiated, but which can be distinguished by MemberRef.
[Example:

.class public C`2<S,T> {
.field string f
.field !0 f
.method instance void m(!0 x) {...}
.method instance void m(!1 x) {...}
.method instance void m(string x) {...}
}

The closed type C``2<string,string> is valid: it has three methods called m, all with the same parameter type; and two fields called f with the same type. They are all distinguished through the MemberRef encoding described above:

string C`2<string, string>::f
!0 C<string, string>::f
void C`2<string, string>::m(!0)
void C`2<string, string>::m(!1)
void C`2<string, string>::m(string)

The way in which a source language might resolve this kind of overloading is left to each individual language. For example, many might disallow such overloads. end example]

In the case of C#, this is not considered illegal and will use standard method overload resolution:

  1. Otherwise, given any two members of the set, M and N, apply the following tie-breaking rules, in order:
    ..snip..
    bv. Before type arguments have been substituted, if M is less generic (Section Genericity) than N, eliminate N from the set.

So the ones which are non-generic are taken in favour of the ones which are generic.

Note that the overload resolution only takes into account information available at compile-time (dynamic excepted). So the constructor

public Map(int resolution, T defaultValue) : this(resolution, resolution, defaultValue) {}

will always call

public Map(int width, int height, T defaultValue) {

and never call this one even for a Map<int>

public Map(int width, int height, int defaultValue) {
Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • If I understand correctly, then if I were to create a new map using `map(width,height)` it would call the 3rd constructor because it doesn't contain a generic value? –  Dec 29 '21 at 22:53
  • Yes correct, the ones with less generic usage will be chosen – Charlieface Dec 29 '21 at 22:54