Подтвердить что ты не робот

Альтернативы typedef или subclassing string в С#

Ситуация

У меня есть класс, который обрабатывает внутри себя много разных типов путей к файлам: некоторые локальные, некоторые удаленные; некоторый относительный, некоторый абсолютный.

Случилось так, что многие из его методов передают их друг другу как string s, но было очень сложно отслеживать, какой тип пути ожидал каждый метод.

Требуемое исправление

Таким образом, мы по существу хотели typedef четыре разных типа: string: RemoteRelative, LocalRelative, RemoteAbsolute и LocalAbsolute. Таким образом, статический тип проверки может помочь разработчикам убедиться, что они обеспечивают и ожидают string с правильной семантикой.

К сожалению, string является sealed в BCL, поэтому мы не могли сделать это с простым наследованием. И нет простого typedef, поэтому мы тоже не могли бы сделать это.

Фактическое исправление

В итоге я создал четыре разных простых класса, каждый из которых содержит readonly string.

public struct LocalAbsolutePath {
    public readonly string path;
    public LocalAbsolutePath(string path) {
        this.path = path;
    }
}

В основном это работает, но в итоге он добавляет немного нежелательной многословности.

Вопрос. Могу ли я игнорировать любые альтернативы, которые естественно вписываются в простой синтаксис С#?

Как я уже упоминал выше, здесь будет моя мечта стиль C typedef string LocalAbsolutePath; или даже стиль F # type LocalAbsolutePath = string. Но даже то, что шаг в этом направлении из пользовательских классов будет большим.

4b9b3361

Ответ 1

Я создал пакет NuGet под названием LikeType, который обеспечивает поведение typedef в классах С#.

Вот как вы его используете:

class CustomerId : LikeType<string>
{
    public CustomerId(string id) : base(id) { }
}

Вот как будет выглядеть тип:

void ShowTypeBehavior()
{
    var customerId = new CustomerId("cust-001"); // create instance with given backing value
    string custIdValue = customerId; // implicit cast from class to backing type, sets 'custIdValue' to "cust-001"

    var otherCustomerId = new CustomerId("cust-002");
    var areEqual = customerId == otherCustomerId; // false
    var areNotEqual = customerId != otherCustomerId; // true
    var areEqualUsingMethod = customerId.Equals(otherCustomerId); // false

    var customerIdCopy = new CustomerId("cust-001"); // create separate instance with same backing value
    var isCopyEqual = customerId == customerIdCopy; // true. Instances are considered equal if their backing values are equal.
}

Ответ 2

Ваше решение хорошее. Вы можете бороться с дополнительной детализацией, добавив преобразование типа в string, позволяя использовать LocalAbsolutePath везде, где может string.

public struct LocalAbsolutePath { // Making it a class would be OK too
    private readonly string path; // <<=== It is now private
    public LocalAbsolutePath(string path) {
        this.path = path;
    }
    public static implicit operator string(LocalAbsolutePath p) {
        return p.path;
    }
}

Ответ 3

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

В то время как я не чувствую, что var myPath = new LocalAbsolutePath("path") на самом деле все более подробный, чем var myPath = "path", поскольку то, что ему не хватает в краткости, оно объясняется в явном виде, но если вы действительно этого хотите, вы можете реализовать неявный оператор литья между вашим классом и строкой и выполните эту работу:

 public static implicit operator LocalAbsolutePath(string path)
 {
     return new LocalAbsolutePath(path);
 }

И теперь вы можете просто сделать:

LocalAbsolutePath myPath = "Path String";

Ответ 4

Поскольку я излагаю те же цели в проекте, над которым я работаю, и я получил большую пользу от ответов здесь, я думал, что разделю решение, в котором я закончил. Работа с нулевым значением, особенно в unit test утверждает, чуть не привела меня в орехи. Конечно, не получится:

string someStringVar = null;
MyStringType myStringType = new MyStringType(someStringVar);
MyStringType myStringTypeNull = null;
Assert.AreEqual(myStringType, myStringTypeNull);

Использование статического Parse() вместо публичного конструктора было более удовлетворительным, потому что оно позволило мне вернуть значение null. Это проходит:

string someStringVar = null;
MyStringType myStringType = MyStringType.Parse(someStringVar);
MyStringType myStringTypeNull = null;
Assert.AreEqual(myStringType, myStringTypeNull);

Кроме того, я не хотел, чтобы неявное преобразование из строки в MyStringType - мне показалось, что я удаляю некоторые из преимуществ сознательного кодера, делая это в первую очередь. Разрешение неявного преобразования из строки будет означать, что вызов метода с параметром MyStringType будет принимать строку, и я не хотел этого, потому что метод с множеством строковых параметров настолько подвержен ошибкам. Обратное преобразование, сделанное неявно, имело для меня больше смысла.

Наконец, я думал, что это идеальный случай для универсального, который был легко использован повторно.

В любом случае, вот что я закончил:

public class StringType<T> where T:class
{
    private readonly string _str;

    protected StringType(string str)
    {
        _str = str;
    }

    public static implicit operator string(StringType<T> obj)
    {
        return obj == null ? null : obj._str;
    }

    public override string ToString()
    {
        return _str;
    }

    public override int GetHashCode()
    {
        return _str.GetHashCode();
    }
}


public class MyStringType : StringType<MyStringType>
{
    protected MyStringType(string str) : base(str) { }

    public static MyStringType Parse(object obj)
    {
        var str = obj is string ? (string)obj : (obj == null ? null : obj.ToString());
        return str == null ? null : new MyStringType(str);
    }
}

Комментарии/улучшения/упрощения, конечно же, приветствуются!

Ответ 5

Возможно, я воткнулся в неправильное дерево, но typedef реализуется как псевдонимы типов на С#, насколько мне известно. Настройка псевдонима типа так же проста, как это:

using LocalAbsolutePath = System.String;

Тогда вы можете начать использовать LocalAbsolutePath как допустимый тип. Более того:

LocalAbsolutePath thisPath = "c:\\thisPath";

Основываясь на вашем посте, я чувствую, что это то, что вы искали. Надеюсь, я прав...!