.net 为什么要强制泛型参数实现接口?[关闭]

z9ju0rcb  于 2023-04-13  发布在  .NET
关注(0)|答案(4)|浏览(230)

已关闭,该问题为opinion-based,目前不接受回答。
**想改进这个问题吗?**更新问题,以便editing this post可以用事实和引用来回答。

7天前关闭
这篇文章7天前被编辑并提交审查。
Improve this question
我正在使用WebAPI,我的API需要调用外部API来获取数据进行处理。
我设计了一个BaseResponse类如下:

public interface IResponseData
{
}

public class BaseResponse<T> where T : IResponseData
{
    public int ResponseId { get; set; }
    public T? Data { get; set; }
}

由于Data必须是某种响应数据,因此T参数必须实现IResponseData
但后来我意识到我可以这样做,而不是摆脱整个通用的部分。

public interface IResponseData
{
}

public class BaseResponse
{
    public int ResponseId { get; set; }
    public IResponseData? Data { get; set; }
}

这里使用where T : IResponseData的意义是什么?是否有第一个例子比第二个好的情况,反之亦然?
对于更多的上下文(这是相当长的文本),目前,我有两种类型的响应,SingleResponseMultipleResponse,这两种类型都实现了IResponseData
我试着这样做:

BaseResponse<IResponseData> baseResponse = new BaseResponse<SingleResponse>();

我得到一个错误,说我不能将SingleResponse转换为IResponseData,尽管SingleResponse实现了IResponseData
我做了一些关于协变的研究,但仍然不明白如何使上面的代码工作。有人能告诉我如何使这个工作吗?(下面有更多详细的上下文信息。)
我之所以写上面的代码是因为:
1.我有一个基于输入参数count处理响应的方法,如果count是1,则SingleRequestSingleResponse,如果count〉1,则MultipleRequestMultipleResponse
1.我必须准备一个请求,调用API,验证响应,并处理两种情况下的响应数据。
1.请求类没有任何共同点,只有响应类有,这就是为什么我创建基类BaseResponse
在此之前,你可以把代码想象成这样:

if (input.Count == 1)
{
  SingleRequest singleRequest = new SingleRequest();
  BaseResponse<SingleResponse> singleResponse = await getSingleAPI();
  Validate(singleResponse);
  Process(singleResponse);
} 
else if (input.Count > 1)
{
  MultipleRequest multipleRequest = new MultipleRequest();
  BaseResponse<MultipleResponse> multipleResponse = await getMultipleAPI();
  Validate(multipleResponse);
  Process(multipleResponse);
}

正如你所看到的,只有请求不同。两种情况的后续过程是相同的。我不想重复代码。所以,我想创建一个BaseResponse<T>,其中T实现IResponseData
现在,我可以在if else之外做这个

BaseResponse<MultipleResponse> response;
if (input.Count == 1)
{
  SingleRequest singleRequest = new SingleRequest();
  response = await getSingleAPI();
} 
else if (input.Count > 1)
{
  MultipleRequest multipleRequest = new MultipleRequest();
  response = await getMultipleAPI();
}

Validate(response)
Process(response)

但是response = await getSingleAPI();response = await getMultipleAPI();不会编译,因为getSingleAPI()返回BaseResponse<SingleResponse>(),getMultipleAPI()返回BaseResponse<MultipleResponse>()
它们产生与BaseResponse<IResponseData> baseResponse = new BaseResponse<SingleResponse>();相同的错误。

olmpazwi

olmpazwi1#

原因之一是:使用BaseResponse的代码可能希望使用实现IResponseData的类,该类包含不属于IResponseData的项。
例如,给定:

public class BaseResponse<T> where T : IResponseData
{
    public int ResponseId { get; set; }
    public T?  Data       { get; set; }
}

public interface IResponseData
{
}

public sealed class MyResponseData: IResponseData
{
    public int ImportantMethod() => 42;
}

你可能有这样的代码:

var baseResponse = new BaseResponse<MyResponseData>
{
    Data = new MyResponseData()
};

MyResponseData result = baseResponse.Data;

Console.WriteLine(result.ImportantMethod());

但如果将BaseResponse类定义更改为

public class BaseResponse
{
    public int            ResponseId { get; set; }
    public IResponseData? Data       { get; set; }
}

则代码将不再编译。
如果可以将type参数的使用限制为接口中项的返回类型(而不是接口中项的参数类型),则可以通过使用the out keyword (generic modifier)来允许一些转换。
举一个例子可以更清楚地说明这一点:
你可以创建一个新的IBaseResponse<T>接口,并将其类型参数声明为out,如下所示:

public interface IBaseResponse<out T> where T: class, IResponseData
{
    int ResponseId { get; set; }
    T? Data { get; }
}

注意这个接口 * 没有 * Data的setter。如果有,代码将无法编译,因为不允许使用out泛型类型作为输入。
无论如何,在创建接口之后,你的类可以像这样实现它:

public class BaseResponse<T> : IBaseResponse<T> where T: class, IResponseData
{
    public int ResponseId { get; set; }
    public T?  Data       { get; set; }
}

假设前面的MyResponseData定义,现在下面的代码可以编译:

var baseResponse = new BaseResponse<MyResponseData>
{
    Data = new MyResponseData()
};

IBaseResponse<IResponseData> test = baseResponse;
IResponseData? data = test.Data;

当然,这并不是您想要的,因为IBaseResponse<IResponseData>.Data没有setter。

mwecs4sa

mwecs4sa2#

一个原因是性能。具有泛型约束的方法将针对每种类型进行专门化。这可能与泛型数学有关,考虑:

public T Add(T a, T b) where T : INumber<T>{
    return a + b
}

如果用doubledecimal分别调用一次,编译器将生成两个版本的方法,每个版本都针对特定类型进行了完全优化。非泛型的Add-方法将导致参数被装箱,以及虚调用以获得正确的add方法。对于数学繁重的代码,这种开销可能会变得很大。
也就是说,绝对存在泛型约束被过度使用的情况,非泛型变体会更好。

njthzxwz

njthzxwz3#

在我看来你在问三个问题:
1.为什么BaseResponse<IResponseData> baseResponse = new BaseResponse<SingleResponse>();不工作
1.如何使它工作
1.何时何地使用遗传学。
以下是答案:
1.因为BaseResponse<IResponseData>不能保证您的对象BaseResponse<SingleResponse>()将在Data属性中设置SingleResponse
1.要使它工作,你应该删除属性的setter。但你可以在实现中使用它。

Ex.:

interface IData {}

class Data : IData {}

interface IDataHolder<out T> where T : IData
{
    T Data { get; }
}

class DataHolder<T> : IDataHolder<T>
    where T : IData
{
    T Data { get; set; }
}

// Then this will work
IDataHolder<IData> dataHolder = new DataHolder<Data>();

1.为了使用一些类特定的方法\属性\等。例如,你有接口ICar与方法void Drive()和实现Truck : ICar也有方法UnhookTrailer()。要访问UnhookTrailer()方法,你需要转换或持有一个对象,正是这种类型。Warehouse只与Truck一起工作,应该只为这个类实现方法,而不是为所有的ICar实现方法。

5rgfhyps

5rgfhyps4#

一般来说。
我使用“泛型约束”的一种方式..它更多的是“高层次的多样性”。
参见:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters
下面是文章中的一句话,它的格式不好,所以请转到上面的URL。但我会留下下面的引用,以防将来URL死亡。
约束描述
其中T:struct类型参数必须是不可为空的值类型。有关可为空的值类型的信息,请参见可为空的值类型。由于所有值类型都具有可访问的无参数构造函数,因此struct约束隐含new()约束,并且不能与new()约束组合。您不能将struct约束与非托管约束组合。
其中T:class类型参数必须是引用类型。此约束也适用于任何类、接口、委托或数组类型。在可空上下文中,T必须是不可空的引用类型。
其中T:class-类型参数必须是引用类型,可以为空或不可为空。此约束也适用于任何类、接口、委托或数组类型。
其中T:notnull类型参数必须是不可为空的类型。参数可以是不可为空的引用类型或不可为空的值类型。
其中T:default此约束解决了在重写方法或提供显式接口实现时需要指定无约束类型参数时的歧义。默认约束意味着没有class或struct约束的基方法。有关详细信息,请参阅默认约束规范提案。
其中T:unmanaged类型参数必须是不可为空的非托管类型。非托管约束意味着struct约束,并且不能与struct或new()约束组合。
其中T:new()类型参数必须具有公共无参数构造函数。与其他约束一起使用时,new()约束必须最后指定。new()约束不能与结构和非托管约束组合。
其中T:〈基类名称〉类型参数必须是或派生自指定的基类。在可空的上下文中,T必须是派生自指定基类的不可空引用类型。
其中T:〈基类名称〉?类型参数必须是指定的基类或从指定的基类派生。在可为空的上下文中,T可以是从指定的基类派生的可为空或不可为空的类型。
其中T:〈interface name〉类型参数必须是或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型。在可空的上下文中,T必须是实现指定接口的不可空类型。
其中T:〈interface name〉?类型参数必须是或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型。在可为空的上下文中,T可以是可为空的引用类型、不可为空的引用类型或值类型。T不能是可为空的值类型。
其中T:U为T提供的类型参数必须是为U提供的参数或从为U提供的参数派生。在可空上下文中,如果U是不可空的引用类型,则T必须是不可空的引用类型。如果U是可空的引用类型,则T可以是可空的或不可空的。
在你的情况下,你有一个“非常具体的约束”,所以你是正确的,这可能是矫枉过正。
我希望你下一个选择

public class BaseResponse
{
    public int ResponseId { get; set; }
    public IResponseData? Data { get; set; }
}

但是对于你的问题....对于泛型约束,有“更有趣”的选择,“而不是一个单一的ISO。”
此外(根据文章)。您可以有多个约束:

class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}

...
最近,我在Enum上创建了一个通用约束。

public class PageResponse<T, E> where E : Enum
{
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public int TotalCount { get; set; }
    public int TotalPages { get; set; }
    public IEnumerable<T> Items { get; set; }
    
    public E OrderByEnum { get; set; }
}

这里我捕获了“单页分页”的结果(就像在REST服务中一样)。
大多数属性都是“用于任何页面请求”(上面的int),但是..其他的东西更灵活。
“Items”(上面的“T”)可以是任何POCO对象。
而“OrderByEnum”可能会有所不同。

public Enum DepartmentSortBy
{
  DepartmentNameAsc,
  DepartmentNameDesc,
  DepartmentCreateDateAsc,
  DepartmentCreateDateDesc
}

public Enum EmployeeSortBy
{
  LastNameAsc,
  LastNameDesc,
  HireDateAsc,
  HireDateDesc
}

所以我想保留“order by”enum“generic”..这样它就可以被重用。
我可以做以下两件事

PageResponse<Department, DepartmentSortBy> pr = new PageResponse<Department, DepartmentSortBy>();

PageResponse<Employee, EmployeeSortBy> pr = new PageResponse<Employee, EmployeeSortBy>();

但我不能

PageResponse<Employee, ArithmeticException> bogus = new PageResponse<Employee, ArithmeticException>();

因为ArithmeticException不是枚举。

相关问题