Как я могу объявить производные классы "оболочки", которые ничего не делают, кроме как переименовывать?

У меня есть два разных типа строк, которые я просматриваю и использую в своем коде, и эти два тесно связаны друг с другом, но их не следует путать друг с другом. Я думал, что могу помочь себе избежать ошибок, имея два класса, которые являются просто строками, но с разными именами, так что сигнатуры методов (и несовместимость типов в целом) будут обеспечивать семантическое значение двух разных типов строк. В то же время я не хотел, чтобы рефакторинг был от something = "foo"; до something.Value = "foo"; в сотне мест.

Первая мысль:

private class FirstKind : string { }
private class SecondKind : string { }

Идея состоит в том, что если у меня есть

void MyMethod(FirstKind varOne, SecondKind varTwo) {...}

а затем попробуйте вызвать его с помощью MyMethod(secondKindVar, firstKindVar);, я получаю ошибку компилятора.

Стена я ударила: string запечатана.

Вторая мысль: создать общий KindOf<T> класс, который ничего не сделал бы, кроме как принять и выплюнуть значение с помощью операторов неявного преобразования. Вот так:

private class KindOf<T>
{
    public T Value { get; set; }
    public KindOf() { Value = default(T); }
    public KindOf(T val) { Value = val; }
    public static implicit operator T(KindOf<T> kindOf) { return kindOf.Value; }
    public static implicit operator KindOf<T>(T value) { return new KindOf<T>(value); }
}

Таким образом я мог бы сделать что-то вроде этого:

private class FirstKind : KindOf<string> { }
private class SecondKind : KindOf<string> { }

Стена я ударил: ничто из класса KindOf<T>, кажется, не наследует: ни конструкторы, ни операторы не существуют в производных типах, то есть я должен повторно реализовать по существу весь базовый класс в производных классах. Копирование. Тьфу.

Таким образом, я чувствую, что мне не хватает чего-то основного здесь, и, возможно, я ухожу в сорняках. Любые идеи для того, что я пытаюсь сделать?

Ответ 1

Если честно, я бы вообще не использовал неявных операторов или какое-либо наследование. Вся идея состоит в том, чтобы иметь метод, который принимает FirstKind. С неявным оператором вы можете просто передать строку, и она будет преобразована; это означает, что вы не заставляете код явно указывать, что это первый/второй вид. Я бы придерживался двух классов, каждый из которых имеет только конструктор, берущий строку и свойство только для чтения. Вы слишком усложняете это, добавив что-нибудь еще.

Если использование этого свойства становится раздражающим, я могу увидеть строку с неявным преобразованием в из каждого пользовательского класса, но я считаю, что неявное преобразование из строка будет пагубной. Хотя я согласен, что код копирования/вставки, как правило, следует избегать, когда он буквально является одной строкой кода, копирование в эти неявное преобразование в оба класса будет проще, эффективнее и, как правило, более легко обслуживаемым, чем попытка использовать наследование или что-то еще чтобы избежать записи дважды.

Ответ 2

Я собираюсь сделать свой комментарий ответом, потому что я думаю, что он стоит как один.

В верхней части моей головы вам может понадобиться реализовать неявные операторы и конструкторы в подклассах. Да, вроде как копирование кода, но, по крайней мере, он не использует свернутый процесс или использует наследование, когда наследование действительно не сообщает ничего реального (риторически, что такое "KindOf", и действительно ли это что-то значит?) И просто там, чтобы уменьшить дублирование кода между классами, которые не имеют ничего общего, кроме аналогичного синтаксиса (но другого использования)

public class FirstKind
{
    public string Value { get; private set; }

    public FirstKind(string value)
    {
        this.Value = value;
    }

    public static implicit operator FirstKind(string value)
    {
        return new FirstKind(value);
    }

    public static implicit operator string(FirstKind value)
    {
        return value.Value;
    }
}

Кроме того, вы можете захотеть сделать класс неизменным, если они должны представлять строки. (еще одна деталь реализации, которая может указывать на, возможно, на эти объекты, не должна наследоваться от повторно используемого класса KindOf, который в вашей реализации делает их изменяемыми всегда)

Это не много проводки и оставляет ваш код открытым для изменений в будущем (возможно, вы хотите добавить новое свойство? Добавьте код проверки?), а потребителям API действительно все равно, что что-либо есть KindOf просто чтобы сэкономить на небольшом дублировании кода. (возможно, вы можете сделать фрагмент, чтобы сделать вашу жизнь немного легче?)

Ответ 3

В вашем вопросе FirstKind и SecondKind - типы ваших строковых переменных, а string - это аргумент типа вашего класса KindOf<T>. Другой подход (который позволяет избежать дублирования кода, требуемого другими ответами) заключается в переводе роли FirstKind и SecondKind, что делает их пустыми "маркерными" классами, которые используются как аргументы типа типа String<TKind>:

public class FirstKind { }
public class SecondKind { }

public struct String<TKind>
{
    private readonly string _value;

    public String(string value)
    {
        _value = value;
    }

    public override string ToString()
    {
        return _value;
    }

    public static implicit operator String<TKind>(string s) // see the note below
    {
        return new String<TKind>(s);
    }

    public static implicit operator string(String<TKind> s)
    {
        return s.ToString();
    }

    public static String<TKind> operator +(String<TKind> s1, String<TKind> s2)
    {
        return new String<TKind>(s1.ToString() + s2.ToString());
    }

    public int Length
    {
        get { return _value.Length; }
    }

    // You can add methods like Substring and Trim that delegate to _value.
}

Теперь вы можете объявить MyMethod следующим образом:

void MyMethod(String<FirstKind> varOne, String<SecondKind> varTwo)
{
    varOne += varOne; // OK
    varTwo += varTwo; // OK
    varOne = varTwo;  // ERROR
}

Примечание. Я оставил оператор неявного преобразования из простого string. Вы можете захотеть удалить его, вынудив код явно указать тип строки: new String<FirstKind>("...").

Ответ 4

Я бы просто сделал это для строк.

abstract class MyTypedString
{
   protected MyTypedString(string value)
   {
     Value = value;
   }
   public string Value { get; private set; }
}

Затем вы можете закончить этот абстрактный класс с помощью FirstKind и SecondKind. Это будет держать объект неизменным, как строки. Я бы не дал никаких имплицитных операторов преобразования.

Ответ 5

Операторы все равно будут работать, вам просто нужно обратиться к .Value члену вашего класса KindOf:

FirstKind stringFoo = new FirstKind("foo");
FirstKind stringQux = new FirstKind("qux");

// String concatenation operator
FirstKind stringTar = new FirstKind( stringFoo.Value + stringQux.Value );

Однако, как вы говорите, не будет "проверки типов", если вы смешаете эти два, например

SecondKind stringBar = new SecondKind("bar");
FirstKind stringHal = new FirstKind( stringFoo.Value + stringBar.Value ); // this will succeed, even though you intend stringFoo and stringBar to be incompatible

Поскольку операторы являются частью интерфейса класса, вы должны повторно реализовать их, но вы можете просто обернуть реализацию содержащегося класса, поэтому вам нужно только выполнить скучную работу grunt, чтобы заставить это работать.

Ответ 6

Вы можете попробовать перегрузить неявный оператор преобразования в свой класс в строку. Тогда вам не придется обновлять свои ссылки на .Value. Классы должны оставаться отдельными, и вам придется создавать для них конструкторы, принимающие строковое значение. Лучшей практикой было бы сохранить строку readonly, чтобы класс стал неизменным. Подражая классу строк, он должен был представлять.

public class FirstKind 
{
      private readonly string value;
      public FirstKind(string v) { this.value = v };
      public static implicit operator string(FirstKind v){ return v.value; }
}

public class SecondKind 
{
      private readonly string value;
      public SecondKind (string v) { this.value = v };
      public static implicit operator string(SecondKind v){ return v.value; }
}

Ответ 7

Вы можете использовать свой общий класс. Только неявное кастинг из строки (или другого типа) в класс derrived никогда не может работать, потому что базовый класс не может знать, должна ли строка быть передана в firstkind или secondkind. Чтобы эта часть была скопирована, остальные могут оставаться в базовом классе:

private class FirstKind : KindOf<string>
{
    public static implicit operator FirstKind(string value) { return new FirstKind{Value = value}; }
}
private class SecondKind : KindOf<string>
{
    public static implicit operator SecondKind(string value) { return new SecondKind{Value = value}; }
}

Отбрасывание в базовый класс все еще можно выполнить из универсального класса:

        FirstKind Foo = "Foo"; //via implicit operator in FirstKind
        string stringFoo = Foo; //via implicit operator in baseclass