因此,我为Mandelbrot Generator编写了此代码,以收集和使用用户的输入,并使它足够动态,可以在任何地方使用。

我对所有批评都非常感兴趣,因为我现在将要大量使用它。

其想法是允许用户轻松地在控制台上显示需要输入的消息。收到有效输入后,该方法将返回。

示例用法:

int numberOfCores = Environment.ProcessorCount - 1;
numberOfCores = Prompt($"Enter the number of cores to use (1 to {Environment.ProcessorCount})",
                       false,
                       numberOfCores,
                       $"The value must be between 1 and {Environment.ProcessorCount}",
                       delegate (int x) { return x >= 1 && x <= Environment.ProcessorCount; });


这将提示用户在1和之间输入一个值。 Environment.ProcessorCount。但是,您也可以使用:

ushort maxIterations = 1000;
maxIterations = Prompt("Enter the maximum number of iterations", false, maxIterations);


只会提示用户输入有效的ushort值。

/// <summary>
/// This will repeatedly prompt the user with a message and request input, then return said input (if valid).
/// </summary>
/// <typeparam name="T">The type of input that should be returned.</typeparam>
/// <param name="message">The message to initally display to the user.</param>
/// <param name="requireValue">Whether or not to allow use of a `defaultValue`.</param>
/// <param name="defaultValue">The default value to be returned if a user enters an empty line (`""`).</param>
/// <param name="failureMessage">The message to display on a failure. If null, then the `message` parameter will be displayed on failure.</param>
/// <param name="validationMethod">An optional delegate to a method which can perform additional validation if the input is of the target type.</param>
/// <returns>The input collected from the user when it is deemed valid.</returns>
static T Prompt<T>(string message, bool requireValue, T defaultValue = default(T), string failureMessage = null, Func<T, bool> validationMethod = null)
    where T : struct
{
    if (!requireValue)
        Console.Write(string.Format(message + " [{0}]: ", defaultValue));
    else
        Console.Write(message + ": ");

    bool pass = false;
    T result = default(T);

    while (!pass)
    {
        string line = Console.ReadLine();

        if (requireValue)
        {
            pass = Retrieve(line, out result);

            if (pass && validationMethod != null)
                pass = validationMethod(result);
        }
        else
        {
            if (line != "")
            {
                pass = Retrieve(line, out result);

                if (pass && validationMethod != null)
                    pass = validationMethod(result);
            }
            else
            {
                pass = true;
                result = defaultValue;
            }
        }

        if (!pass)
        {
            Console.WriteLine("Invalid value [{0}]", line);

            if (failureMessage != null)
            {
                if (!requireValue)
                    Console.Write(string.Format(failureMessage + " [{0}]: ", defaultValue));
                else
                    Console.Write(failureMessage + ": ");
            }
            else
            {
                if (!requireValue)
                    Console.Write(string.Format(message + " [{0}]: ", defaultValue));
                else
                    Console.Write(message + ": ");
            }
        }
    }

    return result;
}

private static bool Retrieve<T>(string line, out T resultValue)
    where T : struct
{
    var type = typeof(T);

    resultValue = default(T);
    bool pass = false;

    if (type == typeof(short))
    {
        short result = 0;
        pass = short.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(int))
    {
        int result = 0;
        pass = int.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(float))
    {
        float result = 0f;
        pass = float.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(double))
    {
        double result = 0f;
        pass = double.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(sbyte))
    {
        sbyte result = 0;
        pass = sbyte.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(byte))
    {
        byte result = 0;
        pass = byte.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(ushort))
    {
        ushort result = 0;
        pass = ushort.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(uint))
    {
        uint result = 0;
        pass = uint.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(long))
    {
        long result = 0;
        pass = long.TryParse(line, out result);
        resultValue = (T)(object)result;
    }
    else if (type == typeof(ulong))
    {
        ulong result = 0;
        pass = ulong.TryParse(line, out result);
        resultValue = (T)(object)result;
    }

    return pass;
}


补充说明:如果有人想在您的项目中使用它,欢迎您。

#1 楼

字符串格式

if (!requireValue)
    Console.Write(string.Format(message + " [{0}]: ", defaultValue));
else
    Console.Write(message + ": ");


如果message包含任何String.Format()格式代码,则此无辜代码将中断。如果它是私有功能,则可以接受,但要使其可重用,必须解决此问题,必须将调用者的输入视为用户输入。它甚至稍微快一点,但是您在这里不必真正在意:

Console.Write(string.Format("{0} [{1}]: ", message, defaultValue));


您已经在使用插值字符串了,那么您也可以在这里使用它们:

Console.Write($"{message} [{defaultValue}]");


现在您还将看到在循环内重复相同的代码,我们很懒,因此我们希望避免重复的代码,仅将其移至while循环内开始。关于循环:必须至少执行一次,然后do / whilewhile更清晰(关于其意图)。

do
{
    if (requireValue) ...
} while (true);


请注意,我也放弃了退出条件,它将在您的代码中使用return语句直接进行处理。

输入转换

是时候读取输入了。最明显的问题是您的转换函数Retrieve()。它是prolix,甚至还不完整(例如decimalchar是什么?)。

只需将其替换为:

var result = Convert.ChangeType(line, typeof(T));


编写一个辅助函数(为简洁起见,为简化代码):

bool TryConvert<T>(string text, bool ignoreIfEmpty, ref T value)
{
    // null is not possible, if it happens we may want ChangeType()
    // to throw ArgumentNullException because it's an actual error...
    if (ignoreIfEmpty && String.IsNullOrWhiteSpace(text)) 
        return true;

    try
    {
        value = (T)Convert.ChangeType(text, typeof(T));
    }
    catch (InvalidCastException) { return false; }
    catch (FormatException) { return false; }
    catch (OverflowException) { return false; }

    return true;
}


使用它:

T value = defaultValue;
if (!TryConvert(Console.ReadLine(), !requireValue, out value)
{
    Console.WriteLine("Input is not valid.");
    continue;
}


验证

您的验证代码也有些复杂,可以简化一下:

if (validationMethod != null && !validationMethod(value))
{
    Console.WriteLine(failureMessage ?? "Input is not valid.");
    continue;
}


结果

所有内容在一起:

static T Prompt<T>(string message, ...
{
    do
    {
        Console.Write(requireValue ? $"{message} [{defaultValue}]: " : $"{message}: ");

        T value = defaultValue;
        if (!TryConvert(Console.ReadLine(), !requireValue, out value)
        {
            Console.WriteLine("Input is not valid.");
            continue;
        }

        if (validationMethod != null && !validationMethod(value))
        {
            Console.WriteLine(failureMessage ?? "Input is not valid.");
            continue;
        }

        return value;
   } while (true);
}


进一步的改进

到目前为止,我们已经进行了函数实现,但是其接口也可能得到改进。首先,您使用的是布尔参数,尤其是当参数列表很长时,您可能需要使用枚举:

var maxIterations = Prompt("Number of iterations", PromptOptions.Required, 1000);


此外,您的函数接受许多参数(其中大多数具有默认值)。如果您希望将默认值保留在适当的位置,我建议添加更多的重载(对于大多数常见情况)。请注意,您还可以假设如果存在默认值,则requireValue为false(以提供两个简化的重载)。

最后更改是您应用的一般约束。将T限制为struct不会阻止函数的用户使用用户定义的值类型(您不知道如何管理):

struct Point { public int X; public int Y }

var result = Prompt<Point>(...


此外,它还排除字符串(许多用户输入是纯文本)。将该约束更改为IConvertibleConvert.ChangeType()将使用它,并且您的函数可以接受可以从字符串转换的任何类型(您甚至可以删除约束,并让转换器使用所有受支持的深奥转换来进行其所有肮脏的游戏,只写您期望的内容您的T的功能接口文档)。

#2 楼

您可以删除所有if-语句,而改用Convert.ChangeType()。或者,使用T4为您生成代码并将其转换为partial类。为了避免类似的重复操作。

有关此Convert.ChangeType()实现的示例,请查看Adriano Repetti的答案。

评论


\ $ \ begingroup \ $
您能显示用Convert.ChangeType()替换ifs的样子,只是为了让将来的学生更容易吗?
\ $ \ endgroup \ $
–deworde
2015年9月9日在10:47

\ $ \ begingroup \ $
@deworde:还有其他一些答案已经包含了它(例如在这里),这样就足够了。我会在答案中链接到它。
\ $ \ endgroup \ $
– Jeroen Vannevel
2015年9月9日在10:55



#3 楼

while (condition)
{
    if (condition) { /* more nesting */ }
}


所有这些嵌套都变得有些难以导航。我在几个地方计算了5个嵌套级别。

我可以避免循环并减少嵌套,如下所示:

if (!condition) { break; }


然后,您知道condition的计算结果仍为true


在这里,您重复了某些if条件:

if (failureMessage != null)
{
    if (!requireValue)
        Console.Write(string.Format(failureMessage + " [{0}]: ", defaultValue));
    else
        Console.Write(failureMessage + ": ");
}
else
{
    if (!requireValue)
        Console.Write(string.Format(message + " [{0}]: ", defaultValue));
    else
        Console.Write(message + ": ");
}


我会写如:

if (!requireValue)
{
    var message = string.Format("{0} [{1}]: ", failureMessage ?? message, defaultValue);
    Console.Write(message);
}
else
{
    var message = string.Format("{0}: ", failureMessage ?? message);
    Console.Write(message);
}



为什么不使用牙套?众所周知,没有使用大括号来召唤猛禽(或者是使用goto?它们都不好。)。


Retrieve<T>(string line, out T resultValue)方法中,您正在复制所有代码的逻辑超越了使用泛型的目的。您应该将其拆分为一堆重载的方法,这些方法应在签名中专门指定类型。

#4 楼

我认为Retrieve并不是一个好名字。



在某些地方,您的TryGetValue / if以否定检查开头,例如elseif (line != "")。我想反过来:肯定的检查更容易阅读。

此外,可以使用if (!requireValue)时避免使用""。事实上,为什么不使用string.Empty?如果很长且非常重复的string.IsNullOrEmpty() / if可以用更短的东西代替,那么为什么不使用它呢?这是我最近使用过的东西,您应该可以将其转换为适合您的情况的东西:

private T GetValue<T>(DataRow dataRow, string columnName)
{
    var value = dataRow[columnName];

    if (value == null || value == DBNull.Value)
        return default(T);

    var targetType = typeof(T);
    if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
        targetType = targetType.GetGenericArguments().First();

    return (T)Convert.ChangeType(value, targetType);
}


#5 楼

代码设计的一项原则是DRY-不要重复自己。此代码在几个地方重复,其模式类似于

if (!requireValue)
        Console.Write(string.Format(message + " [{0}]: ", defaultValue));
    else
        Console.Write(message + ": ");


。这表明应将此代码放入采用requireValuemessagedefaultValue的新方法中。 。

另一个重复的代码是

pass = Retrieve(line, out result);
if (pass && validationMethod != null)
      pass = validationMethod(result);


,应类似地将其提取为方法。

最后在Retrieve()中我们看到:

if (type == typeof(short))
{


为什么这不是switch?但是代码也会重复。应该有一些方法可以解决这个问题。

评论


\ $ \ begingroup \ $
不幸的是:“开关表达式或大小写标签必须是bool,char,string,integral,enum或相应的可为空的类型”。
\ $ \ endgroup \ $
– Der Kommissar
2015年9月9日在2:50

\ $ \ begingroup \ $
不是我建议这样做(Jeroen的回答几乎可以肯定是正确的方法),但是typeof()。ToString()可以使您切换,而不会造成重大信息丢失。
\ $ \ endgroup \ $
–deworde
2015年9月9日上午10:51