最佳实践:如何构建数组 - 标准和命名约定

13

在多维数组结构中,最佳实践是将迭代器放在哪个元素中以及详细元素?

我的大部分编程经验(我主要是出于兴趣)都来自于Google上的教程,所以如果这似乎是一个异常愚蠢的问题,我提前道歉 - 但我确实想开始改进我的代码。

每当我需要创建一个多维数组时,我的命名总是将计数器放在第一个元素中。

例如,如果我有一个单维数组如下:

$myArray['year']=2012;
$myArray['month']='July';
$myArray['measure']=3;
// and so on.

然而,如果我想让这个数组保留一些历史记录的所有者,我会添加另一个维度,并按以下格式进行格式化:

$myArray[$owner]['year']=2012;
$myArray[$owner]['month']='July';
$myArray[$owner]['measure']=3;

编辑:为了确保我的示例不会让人望而却步或者引导方向错误,我基本上是按照以下结构进行的:

$myArray[rowOfData][columnOfData]

现在,我的问题是关于惯例的。我应该改为做以下操作吗?

$myArray['year'][$owner]=2012;
$myArray['month'][$owner]='July';
$myArray['measure'][$owner]=3;

编辑:使用上面的编辑,应该是:

$myArray[columnOfData][rowOfData]
我搜索了有关数组命名规范的文章,但一直找到争论是否将数组命名为复数形式。我一直使用的方式似乎更合乎逻辑,并且我认为它遵循了更像对象的结构,即object->secondaryLevel->detail,但我并不确定我一直以来是不是在弄错了。随着我越来越深入地进行编程,如果我的做法是错误的,我宁愿改变自己的习惯。
是否有一个被接受的标准或者说数组的任何格式都可以?如果你看到别人编写的代码,你会期望哪种格式?我知道任何有意义/直观的结构都是可以接受的。
此外,从迭代的角度来看,以下哪个更直观?
for($i=0;$i<$someNumber;$i++)
{
    echo $myArray[$i]['year'];
    // OR
    echo $myArray['year'][$owner];
}

编辑:我将这篇帖子标记为C#和Java,因为我想得到一些PHP程序员之外的意见。我认为由于数组在许多不同的语言中都被使用,从各种语言的程序员那里获取一些输入会很有好处。


你的第一种方式更有意义,但这取决于...在给定的示例中,$year可能应该是$date,但基本上它应该描述数组包含的内容,例如$table[/* $row */][/* $col */] - T I
7个回答

21
Your question is subjective, in that everyone may have a different approach to the situation you stated, and you are wise to even ask the question; How best to name your variables, classes, etc. Sad to say, I spend more time than I care to admit determining the best variable names that make sense and satisfy the requirements. My ultimate goal is to write code which is 'self documenting'. By writing self-documenting code you will find that it is much easier to add features or fix defects as they arise.
Over the years I have come to find that these practices work best for me:
Arrays: Always plural
I do this so loop control structures make more semantic sense, and are easier to work with.
// With a plural array it's easy to access a single element
foreach ($students as $student) {}

// Makes more sense semantically
do {} (while (count($students) > 0);

对象数组 > 深度多维数组

在您的示例中,您的数组开始变成了3个元素深度的多维数组,虽然Robbie的代码片段是正确的,但它展示了迭代多维数组所需的复杂性。相反,我建议创建对象,可以将其添加到数组中。请注意,以下代码仅供演示,我始终使用访问器。

class Owner
{
    public $year;
    public $measure;
    public $month;
}

// Demonstrative hydration 
for ($i = 1 ; $i <= 3 ; $i++) {

    $owner = new Owner();

    $owner->year = 2012;
    $owner->measure = $i;
    $owner->month = rand(1,12);

    $owners[] = $owner;
}

现在,您只需要迭代一个扁平数组就可以访问所需的数据:
foreach ($owners as $owner) {
    var_dump(sprintf('%d.%d: %d', $owner->month, $owner->year, $owner->measure));
}

这种对象数组的方法之所以很棒,是因为添加增强功能非常容易。如果你想添加所有者姓名怎么办?没问题,只需将成员变量添加到你的类中,并稍微修改一下数据填充即可:
class Owner
{
    public $year;
    public $measure;
    public $month;
    public $name;
}

$names = array('Lars', 'James', 'Kirk', 'Robert');

// Demonstrative hydration 
for ($i = 1 ; $i <= 3 ; $i++) {

    $owner = new Owner();

    $owner->year = 2012;
    $owner->measure = $i;
    $owner->month = rand(1,12);
    $owner->name = array_rand($names);

    $owners[] = $owner;
}

foreach ($owners as $owner) {
    var_dump(sprintf('%s: %d.%d: %d', $owner->name, $owner->month, $owner->year, $owner->measure));
}

请记住,上面的代码片段只是建议,如果您更愿意坚持使用深层多维数组,那么您必须想出一个元素排序安排,以使其对和与您一起工作的人有意义,如果您认为六个月后设置会有问题,那么最好在您还有机会时实施更好的策略。

谢谢Mike,我同意你关于尽可能使用对象的评论。最近我一直在努力按照最佳实践编写代码(例如使用对象),这也是我想知道如果我确实需要使用数组,我想确保使用最好的标准,以便我的代码对他人和自己都很清晰。 - Fluffeh
我希望在我开始编程时就读过这篇文章。干得好,Mike。 - Roberto
在PHP中,我发现当存储数据集合时,对象与数组相比没有真正的优势,因为您无法声明类型以限制输入到属性中的数据,必须编写额外的代码来声明类,您可能会通过拼写错误的名称将值错误地分配给错误的属性(与数组一样,因此它对数据完整性没有帮助),而且属性的迭代/操作也需要额外的代码(http://php.net/manual/en/language.oop5.iterations.php)。 - Dave F
感谢您的出色回答。不幸的是,问题在赏金到期前一天被管理员关闭了。我已经投票重新打开它,以便将赏金授予您,但不确定是否会成功。 - Fluffeh
@Fluffeh:谢谢,是的,我也不确定为什么它被关闭了,我认为这是一个相当相关和深思熟虑的问题。 - Mike Purcell
因为表述清晰且有Metallica的参考,我点了赞。 - Symen Timmermans

3

您希望使自己(以及潜在用户)易于理解您的代码正在做什么,以及编写代码时您在考虑什么。想象一下寻求帮助,想象输出调试代码或想象在上次接触代码12个月后返回修复错误。哪种方式最适合您/他人理解并且最快捷?

如果您的代码要求“对于每年,显示数据”,则第一种方法更为合理。如果您的想法是“我需要将所有措施汇集在一起,然后再进行处理”,那么就选择第二种选项。如果您需要按年份重新排序,则选择第一种方法。

根据您上面的示例,我处理上述问题的方式可能如下:

$array[$year][$month] = $measure;

你不需要一个特定的“measure”元素。或者如果你每个月有两个元素:
$array[$year][$month] = array('measure' => $measure, 'value'=>$value);

or

$array[$year][$month]['measure'] = $measure;
$array[$year][$month]['value'] = $value;

然后你可以执行以下步骤:
for($year = $minYear; $year <= $maxYear; $year++) {  // Or "foreach" if consecutive
    for ($month = 1; $month <= 12; $month++) {
        if (isset($array[$year][$month])) {
             echo $array[$year][$month]['measure'];  // You can also check these exist using isset if required
             echo $array[$year][$month]['value'];
        } else {
             echo 'No value specified'
        }
    }
}

希望这能有助于你的思考。

是的,它既有助于又确认了我的想法。我希望能够得到其他几个回复,以确保每个人都对数组的思考方式相同。我在工作中进行了一些原型制作,我相信我的代码将由开发团队审查,因此我希望尽可能地减少痛苦。谢谢。 - Fluffeh
嗨,感谢您与我分享您对此的见解。除了其他很棒的答案之外,您的洞察力让我更清楚地了解了我从现在开始将如何工作。不幸的是,由于非建设性而关闭了这个问题 :( - Fluffeh

3
你做得很好。你必须意识到,PHP没有真正的多维数组。你看到的是一个包含一维数组的数组。主要数组存储指针(就像你所说的“迭代器”)。因此,按行优先是唯一合理的方式:
在你的特定示例中,你可以将你的二维数组看作包含一组对象,每个对象都有“year”、“month”和“measure”的值(加上主键“owner”)。通过填写主索引,你可以像这样引用二维数组的每一行:$myArray[$owner]。每个这样的值都是一个具有“year”、“month”和“measure”键的三元素数组。换句话说,它与你原来的同一信息的一维数据结构相同!你可以将其传递给处理表中一个行的函数,也可以轻松地对$myArray的行进行排序等。
如果你把索引放反了,就没有办法恢复你的单个记录。没有“切片”符号可以给你整个二维数组的“列”。
现在来谈谈更广泛的视角:
由于你用行和列来提问,注意将“行”索引放在前面使你的数组与矩阵算术兼容。如果你必须使用矩阵进行计算,这是一个巨大的胜利。数据库符号也将记录放在行中,因此反过来做会不必要地使事情复杂化。
C有真正的二维数组和指针数组。指针数组的工作方式与PHP完全相同(尽管只允许数字索引),原因也是一样的:主索引从指针数组中选择,并且次索引只是所指向的数组的索引。C的二维数组也是这样工作的:主索引在左侧,内存中相邻的位置根据次(第二个)索引的一个值而不同(当然,在行末除外)。这使它们与指针数组兼容,因为可以使用单个索引引用二维数组的一行。例如,a[0]是abcd:
        a[.][0] a[.][1] a[.][2] a[.][3]
a[0]:      a       b       c       d    
a[1]:      e       f       g       . 
a[2]:      .       .       .       .    
a[3]:      .       .       .       .    

系统的运作非常流畅,因为主要(行)索引首先出现。Fortran有真正的二维数组,但是右侧有主要索引:内存中相邻位置的值差一个 左侧(第一)索引的值。我发现这很烦人,因为没有子表达式以同样的方式简化为一维数组。(但我有C语言背景,所以肯定有偏见)。
简而言之:你做得很对,这可能不是偶然,而是因为你通过查看编写良好的代码学习到了。

嗨,感谢您与我分享您对此的见解。除了其他很棒的答案之外,您的洞察力让我更清楚地了解了我从现在开始将如何工作。不幸的是,由于非建设性而关闭了这个问题 :( - Fluffeh
是的,我有点困惑。说实话,我不同意这是一个“主观”的问题... 即使最高票答案是这样说的。 - alexis
谢谢您的回复,请随意投票以重新开放问题,这样我就可以颁发我所设定的赏金了 - 在那之后,我并不太在意他们做什么。我得到了一堆很好的答案,并为自己保存了一份副本。最坏的情况是如果它被删除,我仍然可以在博客或其他地方重新发布它。我认为这对像我这样的初学者程序员和许多其他人来说都是很好的信息。 - Fluffeh

2
从我的角度来看,这不是关于数组命名约定的问题,而是关于如何构造数据结构的问题。也就是说:哪些信息应该放在一起 - 以及为什么?要回答这个问题,你必须同时考虑可读性和性能。
从Java开发者的角度来看(你也标记了Java),我不喜欢多维数组,因为它们往往会在大量嵌套的for循环中导致容易出错的索引操作。为了摆脱数组中的第二个维度,我们可以创建额外的对象,将一列的信息封装在其中。
现在需要决定的是,哪些数据应该嵌入到这个封装对象中。在你的情况下,答案很简单:收集一个用户的数据是有意义的,对于任意用户的某个属性的不相关值列表通常没有意义。
即使你不将数据封装到对象中,而是更喜欢使用多维数组,你也应该牢记这些想法,并将数组的最后一个维度(在这种情况下是一列)视为等同于封装对象。你的数据结构应该在所有抽象级别上都有用,并且这通常意味着你将使用在一起的东西组合在一起。

嗨,感谢您与我分享您对此的见解。除了其他很棒的答案之外,您的洞察力让我更清楚地了解了我从现在开始将如何工作。不幸的是,由于非建设性而关闭了这个问题 :( - Fluffeh

1

我认为,当您使用旨在保持为集体的数据时,最好将其存储在单个数据集中(例如关联数组或对象)。以下是可用于存储数据集的结构示例。

在PHP中,作为关联数组:

$owner = array(
    'year' => 2012, 'month' => 'July', 'measure' => 3
);

在C#中,就像哈希表一样:
Hashtable owner = new Hashtable();
owner.Add("year", 2012);
owner.Add("month", "July");
owner.Add("measure", 3);

在Java中,可以使用哈希表:

Hashtable owner = new Hashtable();
owner.put("year", new Integer(2012));
owner.put("month", new String("July"));
owner.put("measure", new Integer(3));

在C#/Java中,作为对象:
public class Owner {
    public int year;
    public string month;
    public int measure;
}

Owner o = new Owner();
o.year = 2012;
o.month = "July";
o.measure = 3;

在C#和Java中,使用对象而不是哈希表的优点在于可以为每个字段/变量声明变量类型(例如int或string),这将有助于防止错误并维护数据完整性,因为当您尝试将错误类型的数据分配给字段时,将抛出错误(或生成警告)。
在PHP中,我发现对象在存储数据集合时与数组相比没有真正的优势,因为标量变量不允许类型提示,这意味着需要额外的代码来检查/限制输入到属性中的数据,需要编写额外的代码来声明类,您可能会通过拼写错误将值错误地分配给错误的属性(与数组一样,因此它对数据完整性没有帮助),迭代/操作属性也需要额外的代码。在PHP中,在转换为/从JSON时,我还发现使用关联数组更容易处理。

根据问题中的代码,当您需要通过其中一个字段或其他条件(例如字段的组合或字段映射到的数据)访问数据时,通常需要向数组添加另一个维度。

这就是哈希表和关联数组在每种语言中更有用的地方。例如,如果您想根据年份和月份将所有者分组,则可以创建关联数组(或哈希表)来完成此操作。

以下PHP示例使用PDO和fetchAll从数据库获取信息:

$sth = $dbh->prepare("SELECT year, month, measure FROM owners");
$sth->execute();

$rows = $sth->fetchAll(PDO::FETCH_ASSOC);

$data = array();
foreach ($rows as $row) {
    $year = $row['year'];
    $month = $row['month'];

    if (!isset($data[$year])) {
        $data[$year] = array();
    }

    if (!isset($data[$year][$month])) {
        $data[$year][$month] = array();
    }

    array_push($data[$year][$month], $row);
}

这些数据在代码中的示例:

$data = array(
    2011 => array(
        'July' => array(
            array('year' => 2011, 'month' => 'July', 'measure' => 1),
            array('year' => 2011, 'month' => 'July', 'measure' => 3)
        ),

        'May' => array(
            array('year' => 2011, 'month' => 'May', 'measure' => 9),
            array('year' => 2011, 'month' => 'May', 'measure' => 4),
            array('year' => 2011, 'month' => 'May', 'measure' => 2)
        )
    ),

    2012 => array(
        'April' => array(
            array('year' => 2012, 'month' => 'April', 'measure' => 7)
        )
    )
);

然后您可以使用键访问数据。

$data[2011]['July'];

// array(
//     array('year' => 2011, 'month' => 'July', 'measure' => 1),
//     array('year' => 2011, 'month' => 'July', 'measure' => 3)
// )

此外,我在创建代表数据集合的对象时,尽量保持它们的最小化。如果您将集合存储在对象中,并开始添加执行集合操作的函数,则需要维护更多的代码。有时这是必要的,例如,如果您需要通过setters限制可以存储的值,但如果您只是将用户数据传递到数据库并再次显示它,则通常不需要数据集合中的高级功能,而且可能更合理地将功能管理在其他地方。例如,View类可以处理显示数据,Model类可以处理提取数据,而Check对象可以验证是否可以保存数据或验证是否可以根据集合中的数据执行操作。

嗨,非常感谢您与我分享您的见解。除了其他出色的答案外,您的洞察力使我对今后的工作有了更清晰的认识。不幸的是,由于不具建设性,这个问题被关闭了:( - Fluffeh

1

我很少在Java中使用数组,同样适用于C#。它们是面向对象的语言,如果您使用类和对象而不是原始结构(如数组),通常可以获得更好的灵活性。

您的示例看起来像一种称为关联数组的交叉引用或查找。您放置元素的方式取决于您将如何使用它。这是特定于问题的设计决策。您需要问自己从什么开始,想要得到什么结果?

看起来您想要检索不同类型,具体取决于您正在查找什么?月份是字符串,年份是整数等。这使得数组或HashMap成为一个糟糕的选择,因为调用代码将需要猜测正在检索的数据类型。更好的设计是将此数据包装在对象中,这将是类型安全的。这就是使用Java和C#的全部意义。

在这种情况下,使用对象将为您提供灵活性,以检查月份是否实际上是真实值。例如,您可以创建一个包含月份的枚举,并在getMonth方法中返回其实例。

另外,我没有足够的声望点来留下评论,但我想回复Dave F的答案。Hashtable是Java早期的遗留类,因此应该避免使用。HashMap是推荐的替代品,两者都实现了Map接口。

在Java早期,集合类被同步以使它们线程安全(Vector、Hashtable等)。这使得这些基本类变得不必要地缓慢并阻碍了性能。如果你现在需要一个同步映射,可以使用HashMap的包装器。

Map m = Collections.synchronizedMap(new HashMap(...)); 

换句话说,除非你正在处理遗留代码,否则没有理由再使用Hashtable了。

1

实际上,如果我们将其概括到所有编程语言,这不仅仅是一个主观问题。因为编程语言并不总是以相同的方式存储数组,所以如果程序员使用了错误的索引,许多编程语言的数组遍历性能会受到影响。据我所知,大多数语言要么是行优先或列优先。如果您要编写需要高性能数字计算的程序,就需要知道它是哪种方式。

例如:C使用行主序,这是通常的做法。当您按行迭代遍历N x N数组时,您可能只会访问内存N次,因为每一行都是一起加载的。因此,对于第一行的第一个元素,您将从内存中获取整个行。对于第一行的第二个元素,您不需要访问内存,因为该行已经加载。但是,如果您决定按列进行,则可能会出现内存问题。对于第一列的第一个元素,您将加载整个行,然后对于第一列的第二个元素,您将加载下一行...等等。一旦缓存空间用完,第一行可能会被丢弃以腾出空间,一旦开始加载第2列中的所有元素,您将不得不重新开始。在Fortran中,这不是问题;相反,您希望以这种方式执行,因为整个列一次性加载而不是整个行。希望这有意义。请参阅我上面链接的维基百科文章以获得更多视觉解释。

对于大多数程序,开发人员最优先考虑的应该是清晰的代码。但是,在性能至关重要的情况下,了解语言如何处理内存可能是必不可少的。好问题。


嗨,感谢您为我分享关于这个问题的见解。除了其他很好的答案之外,您的洞察力让我对现在将如何工作有了更好的理解。可悲的是,这个问题因为不具建设性而被关闭了 :( - Fluffeh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接