我已编写此代码来读取以特定格式编写的CSV文件。我想收集一些关于可以改进的反馈。我正在尝试进入“测试驱动开发”的思想体系,因此我可以通过任何方式使代码更好地与单元测试一起使用。 with是MatchHeaders函数中的switch语句。我确实尝试使用代表属性名称的枚举,但是遇到一个问题,因为它没有恒定的值,并且坦率地说,它不能使代码读得更干净。而不是给出代码。

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string FullAddress { get; set; }
    public string PostCode { get; set; }
    public string TelephoneNumber { get; set; }
    public string EmailAddress { get; set; }
}

public interface ICsvReader
{
    List<Contact> ParseCsv(string filePath);
}

public class Reader : ICsvReader
{
    public List<Contact> ParseCsv(string filePath)
    {
        List<String[]> rows = GetRows(filePath);
        string[] headers = rows[0];

        return MakeContacts(headers, rows);
    }

    private List<String[]> GetRows(string filePath)
    {
        List<Contact> users = new List<Contact>();
        string[] lines = File.ReadAllLines(filePath);
        var csv = from line in lines
                    select (line.Split(',')).ToArray();

        return csv.ToList();
    }

    private List<Contact> MakeContacts(string[] headers, List<string[]> rows)
    {
        List<Contact> contacts = new List<Contact>();
        // If we didn't know the headers we could contain values with their properties in a collection
        // List<List<Dictionary<string, object>>> csv = new List<List<Dictionary<string, object>>>();
        for (int i = 1; i < rows.Count; i++)
        {
            string[] row = rows[i];
            Contact contact = new Contact();
            for (int e = 0; e < row.Length; e++)
            {
                string header = headers[e];
                string value  = row[e];

                contact = MatchHeader(contact, header, value);
            }
            contacts.Add(contact);
        }
        return contacts;
    }

    private Contact MatchHeader(Contact user, string header, string value)
    {
        switch (header.ToLowerInvariant())
        {
            case "firstname":
                user.FirstName = value;
                break;
            case "lastname":
                user.LastName = value;
                break;
            case "dateofbirth":
                user.DateOfBirth = DateTime.Parse(value);
                break;
            case "fulladdress":
                user.FullAddress = value;
                break;
            case "postcode":
                user.PostCode = value;
                break;
            case "telephonenumber":
                user.TelephoneNumber = value;
                break;
            case "emailaddress":
                user.EmailAddress = value;
                break;
            default:
                break;
        }
        return user;
    }
}


评论

您是否考虑过使用现有的库而不是重新发明轮子?

#1 楼

几点评论:



使代码更抽象,请使用IEnumerable代替List

 public interface ICsvReader
 {
    IEnumerable<Contact> ParseCsv(string filePath);
 }



提供一个异步版本

public interface ICsvReader
{
    IEnumerable<Contact> ParseCsv(string filePath);
    Task<IEnumerable<Contact>> ParseCsvAsync(string filePath);
}



使用StreamReader而不是File.ReadAllLines,它具有一个async版本,可用于ParseCsvAsync方法中br />
using(var stream = new StreamReader(filePath)){
 // read all lines
}



尝试对局部变量使用var关键字,它使重构更加容易
>在C#中使用延迟执行的美感
>
考虑@Snowbody有关使用ToList而不是大型开关的建议。


评论


\ $ \ begingroup \ $
#6是错误的,主要是因为原始代码没有多大意义。在您的版本中,csv是IEnumerable >;要获取List ,您需要将select之后的括号从移到before。
\ $ \ endgroup \ $
– svick
2014年6月28日14:05



\ $ \ begingroup \ $
@svick谢谢你的发言,我现在已经对其进行了编辑
\ $ \ endgroup \ $
– Sleiman Jneidi
14年6月28日在16:05

#2 楼

再次只是一些样式上的问题。


使它变得更LINQ样:在一个语句中完成所有列表处理。除非必须,否则请尽量避免在List<>,数组和IEnumerable<>之间进行转换。查找使用forforeach遍历枚举的位置,或在循环中调用.Add()的位置;这些几乎总是可以在LINQ中更优雅地表达。不要将返回类型设为List<>,而是IList<>。 (显然,在构造返回值时,您仍然需要调用new List<>())。
使用Dictionary<String,...>而不是巨型switch。在这种特殊情况下,字典的值类型应为Action<Contact,String>

MatchHeader()不需要返回任何内容;对user摇杆的修改。


#3 楼

我同意已经提出的许多优点。不过,我要特别注意的一件事是您使用抽象和关注点分离。您的总体约定是,您传递文件名,然后依次接收返回的已解析对象的集合-这样做很合理,但是如果您从其他地方(例如文件上传)获得CSV,该怎么办?
您可能有一个只负责将CSV解析为对象的类,该类的接口如下所示:

public interface TextParser
{
    public IEnumerable<Contact> Parse(string text);
    public IEnumerable<Contact> Parse(StreamReader textReader);
}


您的文件访问代码现在在调用代码而不是解析器本身,这使得从任何地方传递CSV文本变得很简单。此外,您现在可以只对解析逻辑进行单元测试,而不必担心模拟文件系统。

这样做的潜在优势是可以使用策略模式。您可能要制作一个解析器,以管道而不是逗号分隔,或者使用传递到构造函数的架构设置固定宽度列的解析器。

接下来,正如新结构所建议的那样,您将需要一种拆分并返回每一行的方法。如果您将策略模式与继承一起使用,则这将是不同实现之间的主要变体,因此您可以使其成为基础中的抽象方法,并从共享方法中调用它。我可能会让此收益率返回每行标题的IDictionary,因此您在文本和Contact业务对象之间具有中间数据结构。

而不是编写大的switch语句,可能想使用反射来查找共享列名称的属性设置器。对于字符串来说,这非常容易,但是您需要检查数字和日期的类型,以便可以应用相关的Parse操作。这使您摆脱了手动映射Contact的麻烦,以便稍后可以修改它而不必更改解析器。

最后,一旦您创建了一个CSV解析器,就很有可能想要创建另一个。显然,这实际上是您想要的:

public interface TextParser<T> where T : class
{
    public IEnumerable<T> Parse(string text);
    public IEnumerable<T> Parse(StreamReader textReader);
}


如果您已经具有上述基于反射的属性设置,则实际上这将是孩子的游戏。现在,您可以将CSV文件解析为相应的业务对象,直到您感到无聊并执行其他操作为止。玩得开心!

#4 楼

这是个人喜好(AMOPP)的问题,但我将包含一个重载,该重载将流作为输入。

。能够从MemoryStream读取对于单元测试非常有用,谁知道呢,我们可能希望将文件作为资源嵌入,或从网络读取。通过使输入成为流,我们可以执行任何这些操作。我会努力摆脱filePath重载,但这可能是太过分了。 :D

除非我们对List有强烈的需求,否则我通常会返回IEnumerable(AMOPP)当前)以从CSV中读取。实现可以是Contact,但是协定(接口)应该是CsvContactReader

性能
实现后,我们遍历文件两次。读取时一次,处理每行一次。我们还必须将每个联系人存储为字符串和联系人。如果处理非常大的文件,这不是必需的,并且会影响性​​能/内存。

一个人可以读取然后处理每一行,而不是全部读取并全部处理。通过使用IContactReader,在阅读过程中,我们无需再存储所有输入行。和布局)可能会发生变化?通常?

除非我们有很多不同的CSV文件要解析,否则提出通用的解决方案通常要比保证的工作多。如果我们只有一个格式固定的文件,那么将字段位置硬编码到类属性通常是一个很好的解决方案。

正如我们所写,我们可以真正改变文件格式,



如果要添加新字段,则必须更改
yield的定义并在开关中添加新条目。
如果我们更改字段
名称,我们需要更改开关。
如果删除字段,则应该
更改开关并更改Contact的定义


给出所有这些,看起来Contact并不能为我们节省很多(如果有的话)简单的位置匹配

例如

public interface IContactReader {
   IEnumerable<Contact> Parse(string filePath);
   IEnumerable<Contact> Parse(Stream inStream);
}


其他选项>如果文件格式更具可变性,则可以使用表达式按名称更新属性(尽管AFAIK区分大小写,这可能是一个问题),或者可以使用MatchHeaders整理内容,但是,除非有强烈的需求,否则我将首先进行位置更新,看看它是否足够。易于实现和维护。

评论


\ $ \ begingroup \ $
MakeContact()例程对我来说就像一个构造函数。
\ $ \ endgroup \ $
–雪体
2014年6月26日20:33

\ $ \ begingroup \ $
@Snowbody Ish。作为AMOPP,我通常不愿意基于单个(可能是瞬时的)用法为业务对象创建构造函数。 public Contact(string [] line)仅是通过将CSV行拆分为string []来创建Contact的副产品。如果我们更改源(例如更改为DB),则要么拥有未使用的ctor,要么必须从DB读取到字符串数组,然后将其传递给ctor。可以这么说,个人偏爱事项。
\ $ \ endgroup \ $
– AlanT
14年6月27日在7:20

#5 楼

命名与功能

顾名思义,ICsvReader应该是大多数CSV Reader的通用接口。但是,合同显示,它更多地是联系人阅读器。

接口应尽可能通用,以便可以在其他地方重用,例如AppointmentReader

public interface ICsvReader<T>
{
    IEnumerable<T> ParseCsv(string filePath);
}


模块化

将大型代码块拆分为较小的代码块以进行重组和易于理解是很好的,但不要过分实现。您有4种方法,ParseCsvGetRows MakeContactsMatchHeader可以将CSV转换为许多联系人。

性能

MatchHeader是影响性能的一种非常低效的方法。它不仅会同时影响一个属性,而且还会对每行的每个属性执行标头匹配。
var indexes = new
{
    FirstName = headers.IndexOf("firstname"),
    LastName = headers.IndexOf("lastname"),
    DateOfBirth = headers.IndexOf("dateofbirth"),
    //...
};


最终结果

public interface ICsvReader<T>
{
    IEnumerable<T> ParseCsv(string filePath);
}

public class Reader : ICsvReader<Contact>
{
    private const char Delimiter = ',';
    private const int HeaderIndex = 0, HeaderRowCount = HeaderIndex + 1;

    public IEnumerable<Contact> ParseCsv(string filePath)
    {
        var rows = File.ReadAllLines(filePath);
        var headers = rows.ElementAt(HeaderIndex).Split(Delimiter)
                          .Select(x => x.ToLowerInvariant()).ToList();

        var indexes = new
        {
            FirstName = headers.IndexOf("firstname"),
            LastName = headers.IndexOf("lastname"),
            DateOfBirth = headers.IndexOf("dateofbirth"),
            //...
        };

        foreach(var row in rows.Skip(HeaderRowCount)
                               .Select(x => x.Split('|').Select(y => y.Trim()).ToList()))
            yield return new Contact
            {
                FirstName = row[indexes.FirstName],
                LastName = row[indexes.LastName],
                DateOfBirth = DateTime.Parse(row[indexes.DateOfBirth]),
                //...
            };
    }
}