防止通用方法中的多播

Prevent multicasting in generic method

提问人:Nick Farsi 提问时间:8/22/2022 更新时间:8/22/2022 访问量:66

问:

我正在制作用于检查输入的通用验证器:

接口:

public interface IInputValidator
{
    bool CanHandle<T>();
    bool Validate<T>(string? input, out T result);
}

实现:

public class IntegerValidator : IInputValidator
{
    public bool CanHandle<T>()
    {
        return typeof(T) == typeof(int);
    }

    public bool Validate<T>(string? input, out T result)
    {
        var isValid = int.TryParse(input, out var res);
        result = (T)(object)res;
        return isValid;
    }
}

然后我抓住我拥有的所有验证器并像这样注入:(接口本身不是通用的,这感觉很方便,因此我不必一个接一个地注入它们,并且能够将它们分组到一个集合中)

private readonly IEnumerable<IInputValidator> _inputValidators;

public CallerClass(IEnumerable<IInputValidator> inputValidators)
{
   _inputValidators = inputValidators;
}

并这样称呼它:

var validator = _inputValidators.First(r => r.CanHandle<int>());
var isInputValid = validator.Validate(userInput, out int id);

除了实现中的这一行外,一切看起来都很好

result = (T)(object)res;

我觉得这里有些不对劲,但不知道如何让它变得更好。不过,它的工作原理是这样的。

C# 泛型 接口 强制转换

评论

1赞 mason 8/22/2022
你认为有什么作用?(T)(object)res
0赞 Nick Farsi 8/22/2022
我觉得我根本不应该在实现中使用 T。我应该使用 int。但是,如果我将 T 外包给接口级别,而不是方法级别,那么我将无法将所有类注入到单个 IEnumerable 中
0赞 Jeremy Lakeman 8/22/2022
泛型在这里感觉是错误的解决方案。由于您的调用方和实现都知道您正在处理 .我看不出泛型如何以任何方式使您的实现更简单。.Validate(userInput, out int id)int.TryParse(input, out var res);int

答:

1赞 David L 8/22/2022 #1

核心问题是,您正在尝试将相应验证器的解析和该验证器的操作组合到同一个通用接口中。

如果您愿意将解析器和验证器功能分成两个接口:

public interface IInputValidatorResolver
{
    bool CanHandle<T>();
    IInputValidator<T> GetValidator<T>();
}

public interface IInputValidator<T>
{
    bool Validate(string? input, out T result);
}

您可以使用 CallerClass 合约中的实例来解析您调用的相应验证程序和强类型,而无需对象强制转换。解析器实现可以创建一个缓存,将 Validator 强制转换为通用实例。IInputValidatorResolverIInputValidator<T>

public class IntegerValidatorResolver : IInputValidatorResolver
{
    public bool CanHandle<T>() => typeof(T) == typeof(int);
    public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;
    
    private static class Cache<T>
    {
        public static readonly IInputValidator<T> Validator = BuildValidator();
        private static IInputValidator<T> BuildValidator() => ((IInputValidator<T>)new IntegerValidator());
    }
}

public class LongValidatorResolver : IInputValidatorResolver
{
    public bool CanHandle<T>() => typeof(T) == typeof(long);
    public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;

    private static class Cache<T>
    {
        public static readonly IInputValidator<T> Validator = BuildValidator();
        private static IInputValidator<T> BuildValidator() => ((IInputValidator<T>)new LongValidator());
    }
}

public class IntegerValidator : IInputValidator<int>
{
    public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}

public class LongValidator : IInputValidator<long>
{
    public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}

您可以使用以下方法对其进行测试:

IEnumerable<IInputValidatorResolver> validatorResolvers = new List<IInputValidatorResolver> { new IntegerValidatorResolver(), new LongValidatorResolver() };
var intValidator = validatorResolvers.First(x => x.CanHandle<int>()).GetValidator<int>();

var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);

var longValidator = validatorResolvers.First(x => x.CanHandle<long>()).GetValidator<long>();

var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);

也就是说,这会产生一个尴尬的合约,如果你首先执行检查,你的调用可能会引发异常。此外,在此实现或当前实现中,您必须遍历解析器/验证器以找到适当的实例,这是不必要的浪费。CanHandle<T>GetValidator<T>

因此,拥有一个知道如何根据 T 类型解析适当验证器的实例可能更有意义,而无需检查。IInputValidatorResolverCanHandle<T>()

public interface IInputValidatorResolver
{
    IInputValidator<T> GetValidator<T>();
}

public class ValidatorResolver : IInputValidatorResolver
{
    public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;

    private static class Cache<T>
    {
        public static readonly IInputValidator<T> Validator = BuildValidator();
        private static IInputValidator<T> BuildValidator()
        {
            if (typeof(T) == typeof(int))
            {
                return ((IInputValidator<T>)new IntegerValidator());
            }
            else if (typeof(T) == typeof(long))
            {
                return ((IInputValidator<T>)new LongValidator());
            }
            else
            {
                throw new ArgumentException($"{typeof(T).FullName} does not have a registered validator.");
            }
        }
    }
}

public interface IInputValidator<T>
{
    bool Validate(string? input, out T result);
}

public class IntegerValidator : IInputValidator<int>
{
    public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}

public class LongValidator : IInputValidator<long>
{
    public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}

这允许更干净的 API,并且注册和枚举要少得多:

IInputValidatorResolver resolver = new ValidatorResolver();
var intValidator = resolver.GetValidator<int>();

var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);

var longValidator = resolver.GetValidator<long>();

var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);

更新看起来你想要一个纯粹的构造函数注入驱动的解决方案。为此,可以将新接口注册为并将其注入解析程序实例。IEnumerable<IInputValidator>

该接口负责在强制转换为 之前检查的方法。IInputValidatorCanHandle<T>()IInputValidator<T>

public interface IInputValidatorResolver
{
    IInputValidator<T> GetValidator<T>();
}

public class ValidatorResolver : IInputValidatorResolver
{
    private IEnumerable<IInputValidator?> _validators;
    
    public ValidatorResolver(IEnumerable<IInputValidator?> validators)
    {
        _validators = validators;
    }
    
    public IInputValidator<T> GetValidator<T>()
    {
        foreach (var validator in _validators)
        {
            if (validator!.CanHandle<T>())
            {
                return (IInputValidator<T>)validator;
            }
        }
        
        throw new ArgumentException($"{typeof(T).FullName} does not have a registered validator.");
    }
}

public interface IInputValidator 
{
    bool CanHandle<TInput>();   
}

public interface IInputValidator<T> : IInputValidator
{
    bool Validate(string? input, out T result);
}

public class IntegerValidator : IInputValidator<int>
{
    public bool CanHandle<T>() => typeof(T) == typeof(int);
    public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}

public class LongValidator : IInputValidator<long>
{
    public bool CanHandle<T>() => typeof(T) == typeof(long);
    public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}

您可以使用以下命令测试此行为:

var servicesCollection = new ServiceCollection();
servicesCollection.AddTransient(typeof(IEnumerable<IInputValidator>), s =>
{
    return new List<IInputValidator>
    {
            new IntegerValidator(),
            new LongValidator()
    };
});
servicesCollection.AddTransient<IInputValidatorResolver, ValidatorResolver>();

var serviceProvider = servicesCollection.BuildServiceProvider();

var resolver = serviceProvider.GetService<IInputValidatorResolver>();
var intValidator = resolver.GetValidator<int>();

var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);

var longValidator = resolver.GetValidator<long>();

var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);

使用此方法,不再需要 ValidationResolver。它只是封装 的逻辑的一种方式。你可以很容易地注入到你的消费类中,并在每次需要使用它时执行强制转换。GetValidator<T>IEnumerable<IInputValidator>

评论

0赞 Nick Farsi 8/22/2022
感谢您的详细解释!但是将静态类作为缓存看起来很奇怪?我虽然你应该避免这种模式,而不是手动实例化类,但必须将它们注入构造函数中。但是,由于您使用泛型接口,因此无法将它们注入单个 IEnumerable 中,而这正是我试图避免的
1赞 David L 8/22/2022
将静态泛型类作为缓存的原因是,您可以为每个 T 实例缓存一个验证器,这非常高效。验证实例中没有任何内容表明它需要将 DI 直接放入使用类中,并且通过 IInputValidatorResolver 进行抽象,该抽象仍允许您根据需要模拟实现。也就是说,如果您完全反对,则可以添加一个额外的非泛型接口用于 DI 注册,同时强制转换为解析器中的目标类型化验证器。我已经更新了我的答案。
0赞 Nick Farsi 8/22/2022 #2

另一种选择是使用 Autofac 的 IComponentContext:

using Autofac;
using TransactionStorage.Interface;

namespace TransactionStorage.Core
{
    public class InputResolver : IInputResolver 
    {
        private readonly IComponentContext _context;

        public InputResolver (IComponentContext context)
        {
            _context = context;
        }

        public bool Validate<T>(string? userInput, out T result) where T : struct
        {
            var validator = _context.Resolve<IInputValidator<T>>();
            return validator.Validate(userInput, out result);
        }
    }
}