避免 C 中的副作用

Avoiding Side Effects in C

这个问题特别适用于数组。

在许多语言中,我会这样做:

    #This is in python for simplicity:

    def increment(mylist):
        for i in mylist:
            i += 1;
            
        return mylist
    

    mylist = {0,1,2,3}
    mylist = increment(mylist)

我尝试了几种在 C 中 return 数组的方法,其中 none 的工作方式与上述相同。似乎 C 根本不打算以这种方式工作。相反,我必须这样做:

#include <stdio.h>

increment(int *myarray, int size) {
    for(int i = 0; i < size; i++){
        myarray[i] += 1;
        
    }
}

int main(){
    int myarray[4] = {0,1,2,3};
    increment(myarray, 4);

}

不用说,C函数改变了数组的状态,因此是一个副作用函数。有充分的理由避免这种情况(这不是这个问题的主题)。

有没有办法在 C 中避免这些类型的副作用?

首先,在python中{0,1,2,3}不是列表而是集合。

与您在 C 中所做的更直接等效的 python 代码是:

def increment(mylist):
    for i in range(len(mylist)):
        mylist[i] += 1;
        
    return mylist


mylist = [0,1,2,3]
mylist = increment(mylist)

在这种情况下,python 中的列表也会产生副作用。 这是因为传递数组的最常见方式是通过引用(或 C 中的指针)

更接近您在 python 代码中所做的 C 代码是:

void increment(int *myarray, int size) {
    for(int i = 0; i < size; i++){
        int v = myarray[i]; // copy of the array value here
        v += 1;
    }
}

int main(){
    int myarray[4] = {0,1,2,3};
    increment(myarray, 4);
}

在那种情况下,数组也没有副作用,因为我只是在使用它之前复制了数组值。

如果您想避免副作用,一般规则是您必须复制数组或单个数组值。

编辑:您可能想在 python 函数中做的是

def increment(mylist):
    mylist = list(mylist) # copy array
    for i in range(len(mylist)):
        mylist[i] += 1
    return mylist

按照正式定义,更改数组的内容在 C 语言中始终是“副作用”。如果您更愿意寻找一种方法来制作数组等不可变,就像在只读中一样,并且总是在操作时创建一个新对象,也有一些方法可以做到这一点。

您必须知道,这通常涉及数据内容的“硬拷贝”,因此会带来执行开销。如果您不想,C 可以让您选择不那么低效。但如果你想要它,那么更灵活的选择是动态分配。像这样:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int* increment(const int* myarray, int size) 
{
  int* new_obj = malloc(sizeof(int[size]));
  for(int i=0; i<size; i++)
  {
    new_obj[i] = myarray[i] + 1;
  }
  return new_obj;
}

int main (void)
{
  int* myarray = malloc(sizeof(int[4]));
  memcpy(myarray, (int[]){0,1,2,3}, sizeof(int[4]));
  for(int i=0; i<4; i++)
  {
    printf("%d ", myarray[i]);
  }
  puts("");
  
  int* another_array = increment(myarray, 4);
  free(myarray);
  
  for(int i=0; i<4; i++)
  {
    printf("%d ", another_array[i]);
  }

  free(another_array);
}

请注意,这比就地修改原始数组要慢得多。堆分配和数据拷贝都比较慢

虽然您可以在 C 中创建“糟糕的 API”函数,例如

int* increment(int *myarray, int size) {
    for(int i = 0; i < size; i++){
        myarray[i] += 1;
        
    }
    return myarray;
}

此 returns 指向传递的同一数组的指针。这很糟糕 API 因为它令人困惑,尽管一些 C 标准函数就是这样设计的(strcpy 等)。为了使用这个函数,你需要一个指向数组第一个元素的指针,而不是数组本身。

在 C 代码中,您将指针传递给数组的第一个元素,而数组保留在内存中。您可以做的是创建一个新数组,然后 return 一个指向它的指针。但是,请小心。如果你创建一个 auto 数组(在栈上创建),它只会存在于函数内部,所以 returned 指针将指向垃圾内存。

int* increment(int *myarray, int size) {
    int tempArray[size]; //only exists inside of the function.
    for(int i = 0; i < size; i++){
        tempArray[i] = myarray[i] + 1;
    }
    return tempArray; //don't do this, tempArray will not exist outside of this function.
}

您可以改用 malloc 函数,它使用堆内存,并且也存在于函数之外。 (您需要包括 stdlib.h)

#include <stdio.h>
#include <stdlib.h>

int* increment(int *myarray, int size) {
    int* tempArray = malloc(size*sizeof(int)); //exists globally
    for(int i = 0; i < size; i++){
        tempArray[i] = myarray[i] + 1;
    }
    return tempArray;
}

int main(){
    int myarray[4] = {0,1,2,3};
    int* newarray = increment(myarray, 4);
    //use the newarray - myarray stays the same.
    free(newarray); //don't forget to free when you no longer need it
}

注意,在 python 中,对象是通过引用传递的。

def increment(mylist):
    // mylist is a local reference to the original array
    for i in mylist:
        i += 1;   // i is a local value: nothing is changed in mylist!
        
    return mylist  // returns a reference to the original (and unchanged...) array

要更改原始列表应该做什么:

def increment(mylist):
    for i in range(len(mylist)):
        mylist[i] += 1
    // returning mylist is optional since the caller's list has been modified

这与

的 C 语言完全等价
int *increment(int array[], int size) {
    for (int i=0; i<size; ++i) {
        array[i] += 1;
    }
    return array;
}

但是您可以通过这种方式在 Python 中构建并 return 一个全新的列表:

def increment(mylist):
    return [i + 1 for i in mylist]

这在 C 中不容易完成。惯用的方法是让调用者提供数组和大小(如上所述)或 return 动态分配的数组:

int *increment(int array[], int size) {
    int *new_array = malloc(size * sizeof(int));
    for (int i=0; i<size; ++i) {
        new_array[i] = array[i] + 1;
    }
    return new_array;
}

并让调用者在通过转让所有权完成后释放 returned 数组。