65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 扩展方法将 List 转换为分隔符文本字符串

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.61/5 (9投票s)

2015年10月13日

CPOL
viewsIcon

35429

一个简单的 C# 扩展方法,可以将 List 转换为分隔符文本字符串。非常适合创建 CSV 文件!

引言

这个技巧展示了一个快速的 C# 扩展方法,可以被调用将对象列表转换为分隔符文本字符串。这可能非常适合将对象列表转换为 string,用于 CSV 文件,每行一个对象,每个 public 属性一个字段。

背景

我今天需要使用这个,所以想与可能也需要它的人分享。

Using the Code

你可以像这样调用扩展方法,它将返回一个使用行分隔符和字段分隔符格式化的 string

myList.ToDelimitedText(delimiter, includeHeader, trimTrailingNewLineIfExists);

我编写了一套简短的测试用例来展示扩展方法的使用…

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Gists.Extensions.ListOfTExtentions;

namespace Gists_Tests.ExtensionTests.ListOfTExtentionTests
{
    [TestClass]
    public class ListOfT_ToDelimitedTextTests
    {
        #region Mock Data

        private class ComplexObject
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public bool Active { get; set; }
        }

        #endregion

        #region Tests

        [TestMethod]
        public void ToDelimitedText_ReturnsCorrectNumberOfRows()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true}
            };
            const string delimiter = ",";
            const int expectedRowCount = 3;
            const bool includeHeader = false;
            const bool trimTrailingNewLineIfExists = true;

            // ACT
            string result = itemList.ToDelimitedText
		(delimiter, includeHeader, trimTrailingNewLineIfExists);
            var lines = result.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
            var actualRowCount = lines.Length;

            // ASSERT
            Assert.AreEqual(expectedRowCount, actualRowCount);
        }

        [TestMethod]
        public void ToDelimitedText_IncludesHeaderRow_WhenSet()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true}
            };
            const string delimiter = ",";
            const int expectedRowCount = 4;
            const bool includeHeader = true;
            const bool trimTrailingNewLineIfExists = true;
            const string expectedHeader = @"Id,Name,Active";
            
            // ACT
            string result = itemList.ToDelimitedText
		(delimiter, includeHeader, trimTrailingNewLineIfExists);
            var lines = result.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
            int actualRowCount = lines.Length;
            string actualFirstRow = lines[0];
            
            // ASSERT
            Assert.AreEqual(expectedRowCount, actualRowCount);
            Assert.AreEqual(expectedHeader, actualFirstRow);
        }

        [TestMethod]
        public void ToDelimitedText_ReturnsCorrectNumberOfProperties()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true}
            };
            const string delimiter = ",";
            const int expectedPropertyCount = 3;

            // ACT
            string result = itemList.ToDelimitedText(delimiter);
            var lines = result.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
            var properties = lines.First().Split(delimiter.ToCharArray());
            var actualPropertyCount = properties.Length;

            // ASSERT
            Assert.AreEqual(expectedPropertyCount, actualPropertyCount);
        }

        [TestMethod]
        public void ToDelimitedText_RemovesTrailingNewLine_WhenSet()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true},
            };
            const string delimiter = ",";
            const bool includeHeader = false;
            const bool trimTrailingNewLineIfExists = true;

            // ACT
            string result = itemList.ToDelimitedText
		(delimiter, includeHeader,trimTrailingNewLineIfExists);
            bool endsWithNewLine = result.EndsWith(Environment.NewLine);

            // ASSERT
            Assert.IsFalse(endsWithNewLine);
        }

        [TestMethod]
        public void ToDelimitedText_IncludesTrailingNewLine_WhenNotSet()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true},
            };
            const string delimiter = ",";

            // ACT
            string result = itemList.ToDelimitedText(delimiter);
            bool endsWithNewLine = result.EndsWith(Environment.NewLine);

            // ASSERT
            Assert.IsTrue(endsWithNewLine);
        }

        #endregion
    }
}

实际的扩展方法代码如下所示

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace Gists.Extensions.ListOfTExtentions
{
    public static class ListOfTExtentions
    {
        /// <summary>
        /// Converts this instance to delimited text.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="instance">The instance.</param>
        /// <param name="delimiter">The delimiter.</param>
        /// <param name="includeHeader">
        /// if set to <c>true</c> then the header row is included.
        /// </param>
        /// <param name="trimTrailingNewLineIfExists">
        /// If set to <c>true</c> then trim trailing new line if it exists.
        /// </param>
        /// <returns></returns>
        public static string ToDelimitedText<T>(this List<T> instance,
            string delimiter,
            bool includeHeader = false,
            bool trimTrailingNewLineIfExists = false)
            where T : class, new()
        {
            int itemCount = instance.Count;
            if (itemCount == 0) return string.Empty;

            var properties = GetPropertiesOfType<T>();
            int propertyCount = properties.Length;
            var outputBuilder = new StringBuilder();

            AddHeaderIfRequired(outputBuilder, includeHeader, properties, propertyCount, delimiter);

            for (int itemIndex = 0; itemIndex < itemCount; itemIndex++)
            {
                T listItem = instance[itemIndex];
                AppendListItemToOutputBuilder
                (outputBuilder, listItem, properties, propertyCount, delimiter);

                AddNewLineIfRequired(trimTrailingNewLineIfExists, itemIndex, itemCount, outputBuilder);
            }

            var output = outputBuilder.ToString();
            return output;
        }

        private static void AddHeaderIfRequired(StringBuilder outputBuilder,
            bool includeHeader,
            PropertyInfo[] properties,
            int propertyCount,
            string delimiter)
        {
            if (!includeHeader) return;

            for (int propertyIndex = 0; propertyIndex < properties.Length; propertyIndex += 1)
            {
                var property = properties[propertyIndex];
                var propertyName = property.Name;
                outputBuilder.Append(propertyName);

                AddDelimiterIfRequired(outputBuilder, propertyCount, delimiter, propertyIndex);
            }
            outputBuilder.Append(Environment.NewLine);
        }

        private static void AddDelimiterIfRequired
        (StringBuilder outputBuilder, int propertyCount, string delimiter,
            int propertyIndex)
        {
            bool isLastProperty = (propertyIndex + 1 == propertyCount);
            if (!isLastProperty)
            {
                outputBuilder.Append(delimiter);
            }
        }

        private static void AddNewLineIfRequired
        (bool trimTrailingNewLineIfExists, int itemIndex, int itemCount,
            StringBuilder outputBuilder)
        {
            bool isLastItem = (itemIndex + 1 == itemCount);
            if (!isLastItem || !trimTrailingNewLineIfExists)
            {
                outputBuilder.Append(Environment.NewLine);
            }
        }

        private static void AppendListItemToOutputBuilder<T>(StringBuilder outputBuilder,
            T listItem,
            PropertyInfo[] properties,
            int propertyCount,
            string delimiter)
            where T : class, new()
        {

            for (int propertyIndex = 0; propertyIndex < properties.Length; propertyIndex += 1)
            {
                var property = properties[propertyIndex];
                var propertyValue = property.GetValue(listItem);
                outputBuilder.Append(propertyValue);

                AddDelimiterIfRequired(outputBuilder, propertyCount, delimiter, propertyIndex);
            }
        }

        private static PropertyInfo[] GetPropertiesOfType<T>() where T : class, new()
        {
            Type itemType = typeof(T);
            var properties = itemType.GetProperties
            (BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public);
            return properties;
        }
    }
}

历史

  • 2015-10-16 更新,包含根据建议的标题行
  • 2015-10-13 初始草稿
© . All rights reserved.